// // LoginViewController.swift // VenusKitto // // Created by Neoa on 2025/8/21. // import Foundation import UIKit import WebKit class LoginViewController: UIViewController { // UI Elements var logoImageView: UIImageView! var phoneNumberTextField: UITextField! var verificationCodeTextField: UITextField! var getVerificationCodeButton: UIButton! var termsCheckboxButton: UIButton! var loginButton: UIButton! var termsLabel: UILabel! // State var isTermsAccepted: Bool = false var customAlertView: UIView! var alertBackground: UIView! // State var isCountdownActive: Bool = false var countdownTimer: Timer? var remainingTime: Int = 60 var uuid: String? override func viewDidLoad() { super.viewDidLoad() // Set up the view background color view.backgroundColor = .white if let backImage = UIImage(named: "AddPet385") { let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel)) navigationItem.leftBarButtonItem = backButton } let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) view.addGestureRecognizer(tapGesture) // Set up the logo logoImageView = UIImageView() logoImageView.image = UIImage(named: "cats378") logoImageView.contentMode = .scaleAspectFit logoImageView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(logoImageView) // Set up phone number text field phoneNumberTextField = UITextField() phoneNumberTextField.placeholder = "请输入手机号码" phoneNumberTextField.keyboardType = .numberPad phoneNumberTextField.borderStyle = .roundedRect phoneNumberTextField.translatesAutoresizingMaskIntoConstraints = false view.addSubview(phoneNumberTextField) // Set up verification code text field verificationCodeTextField = UITextField() verificationCodeTextField.placeholder = "请输入验证码" verificationCodeTextField.keyboardType = .numberPad verificationCodeTextField.borderStyle = .roundedRect verificationCodeTextField.translatesAutoresizingMaskIntoConstraints = false view.addSubview(verificationCodeTextField) // Set up get verification code button getVerificationCodeButton = UIButton(type: .system) getVerificationCodeButton.setTitle("获取验证码", for: .normal) getVerificationCodeButton.titleLabel?.font = UIFont.systemFont(ofSize: 13) getVerificationCodeButton.setTitleColor(UIColor(hex: "#000000"), for: .normal) getVerificationCodeButton.translatesAutoresizingMaskIntoConstraints = false getVerificationCodeButton.addTarget(self, action: #selector(getVerificationCode), for: .touchUpInside) view.addSubview(getVerificationCodeButton) // Set up terms checkbox button termsCheckboxButton = UIButton(type: .custom) termsCheckboxButton.setImage(UIImage(named: "cats370"), for: .normal) termsCheckboxButton.setImage(UIImage(named: "cats371"), for: .selected) termsCheckboxButton.addTarget(self, action: #selector(toggleTermsAcceptance), for: .touchUpInside) termsCheckboxButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(termsCheckboxButton) // Set up terms label with clickable links termsLabel = UILabel() termsLabel.font = UIFont.systemFont(ofSize: 12) termsLabel.textColor = .gray termsLabel.numberOfLines = 0 termsLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(termsLabel) // Set up the clickable parts of the text with specific color and links let fullText = "我已阅读并同意《中国认证服务条款》以及《用户协议》和《隐私政策》" let attributedText = NSMutableAttributedString(string: fullText) // Define ranges let range1 = (fullText as NSString).range(of: "中国认证服务条款") let range2 = (fullText as NSString).range(of: "用户协议") let range3 = (fullText as NSString).range(of: "隐私政策") let linkColor = UIColor(hex: "#FFE059") // Set color and underline for clickable text attributedText.addAttribute(.foregroundColor, value: linkColor, range: range1) attributedText.addAttribute(.foregroundColor, value: linkColor, range: range2) attributedText.addAttribute(.foregroundColor, value: linkColor, range: range3) // Add underline style to indicate clickable attributedText.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range1) attributedText.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range2) attributedText.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range3) // Add link attribute with custom scheme for tap detection attributedText.addAttribute(.link, value: "terms://service", range: range1) attributedText.addAttribute(.link, value: "terms://user", range: range2) attributedText.addAttribute(.link, value: "terms://privacy", range: range3) // Use UITextView instead of UILabel to support clickable links let termsTextView = UITextView() termsTextView.attributedText = attributedText termsTextView.font = UIFont.systemFont(ofSize: 12) termsTextView.textColor = .gray termsTextView.isEditable = false termsTextView.isScrollEnabled = false termsTextView.backgroundColor = .clear termsTextView.translatesAutoresizingMaskIntoConstraints = false termsTextView.dataDetectorTypes = [] termsTextView.delegate = self termsTextView.textContainerInset = .zero termsTextView.textContainer.lineFragmentPadding = 0 termsTextView.linkTextAttributes = [ .foregroundColor: UIColor(hex: "#000000"), .underlineStyle: NSUnderlineStyle.single.rawValue ] view.addSubview(termsTextView) self.termsLabel = nil // Remove the UILabel reference since using UITextView // Set up login button loginButton = UIButton(type: .system) loginButton.setTitle("登录", for: .normal) loginButton.backgroundColor = UIColor(hex: "#FFE059") loginButton.setTitleColor(.black, for: .normal) loginButton.layer.cornerRadius = 25 loginButton.addTarget(self, action: #selector(login), for: .touchUpInside) loginButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(loginButton) // Set up constraints NSLayoutConstraint.activate([ logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40), logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), logoImageView.widthAnchor.constraint(equalToConstant: 100), logoImageView.heightAnchor.constraint(equalToConstant: 100), phoneNumberTextField.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 40), phoneNumberTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), phoneNumberTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), phoneNumberTextField.heightAnchor.constraint(equalToConstant: 40), verificationCodeTextField.topAnchor.constraint(equalTo: phoneNumberTextField.bottomAnchor, constant: 20), verificationCodeTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), verificationCodeTextField.trailingAnchor.constraint(equalTo: getVerificationCodeButton.leadingAnchor, constant: -10), verificationCodeTextField.heightAnchor.constraint(equalToConstant: 40), getVerificationCodeButton.centerYAnchor.constraint(equalTo: verificationCodeTextField.centerYAnchor), getVerificationCodeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), getVerificationCodeButton.widthAnchor.constraint(equalToConstant: 100), getVerificationCodeButton.heightAnchor.constraint(equalToConstant: 40), termsCheckboxButton.topAnchor.constraint(equalTo: verificationCodeTextField.bottomAnchor, constant: 20), termsCheckboxButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), termsCheckboxButton.widthAnchor.constraint(equalToConstant: 20), termsCheckboxButton.heightAnchor.constraint(equalToConstant: 20), termsTextView.topAnchor.constraint(equalTo: verificationCodeTextField.bottomAnchor, constant: 20), termsTextView.leadingAnchor.constraint(equalTo: termsCheckboxButton.trailingAnchor, constant: 10), termsTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), termsTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 40), loginButton.topAnchor.constraint(equalTo: termsTextView.bottomAnchor, constant: 30), loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), loginButton.heightAnchor.constraint(equalToConstant: 50) ]) } @objc private func tapCancel() { self.dismiss(animated: true) } @objc func dismissKeyboard() { // Dismiss the keyboard when tapping outside the text fields view.endEditing(true) } // MARK: - Actions @objc func getVerificationCode() { // Logic to handle verification code request print("获取验证码") if isCountdownActive { return // If countdown is already active, do nothing } // Validate phone number guard let phoneNumber = phoneNumberTextField.text, !phoneNumber.isEmpty else { // Show error if phone number is empty showError("请输入手机号码") return } guard isValidPhoneNumber(phoneNumber) else { showAlert(title: "提示", message: "手机号格式不正确") return } // Send POST request to the server with phone number let url = URL(string: "\(baseURL)/petRecordApUser/sendSmsCode")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let parameters = ["phonenumber": phoneNumber] request.httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: .fragmentsAllowed) print("Request URL: \(url)") print("Request Parameters: \(parameters)") // Make the network request URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { DispatchQueue.main.async { self.showError("请求失败: \(error.localizedDescription)") } return } // Log the response status code if let response = response as? HTTPURLResponse { print("Response Status Code: \(response.statusCode)") } // Handle the response here (e.g., check the status code or response) DispatchQueue.main.async { if let data = data { print("ssss \(data)") // Example response handling (customize this part as per your API response) if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { // 验证码发送成功 if let dataDict = json["data"] as? [String: Any], let uuid = dataDict["uuid"] as? String { print("验证码发送成功,UUID: \(uuid)") // 保存uuid到属性中 self.uuid = uuid // Start the countdown self.startCountdown() } else { self.uuid = "uuidstring" let errorMsg = json["msg"] as? String ?? "登录失败,未知错误" self.showAlert(title: "发送失败", message: errorMsg) } // Print the entire JSON object print("Response JSON: \(json)") // If you want to print specific values from the JSON, you can access them like this: // if let uuid = json["uuid"] as? String { // print("UUID received: \(uuid)") // self.uuid = uuid // } // You can also print other parts of the response if let message = json["msg"] as? String { print("Server Message: \(message)") } } } } }.resume() } @objc private func loginTapped() { guard let phone = phoneNumberTextField.text, !phone.isEmpty else { showAlert(title: "提示", message: "请输入手机号") return } guard isValidPhoneNumber(phone) else { showAlert(title: "提示", message: "手机号格式不正确") return } guard let code = verificationCodeTextField.text, !code.isEmpty else { showAlert(title: "提示", message: "请输入验证码") return } // Ensure UUID is available (it should be set when the SMS code was requested) guard let uuid = self.uuid else { showAlert(title: "错误", message: "请先获取验证码") return } // Prepare the request parameters let parameters: [String: Any] = [ "phonenumber": phone, "smsCode": code, "uuid": uuid // Pass the uuid received during the SMS request ] // Create the URL for the login API guard let url = URL(string: "\(baseURL)/petRecordApUser/phoneLogin") else { showAlert(title: "错误", message: "无效的URL") return } // Create the request var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Log the request URL and parameters print("Login Request URL: \(url)") print("Login Request Parameters: \(parameters)") // Add the JSON body to the request do { request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) } catch { showAlert(title: "错误", message: "参数编码失败: \(error.localizedDescription)") return } // Show activity indicator while making the network request let activityIndicator = UIActivityIndicatorView(style: .medium) activityIndicator.center = view.center view.addSubview(activityIndicator) activityIndicator.startAnimating() // Send the request using URLSession let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in // Ensure UI updates happen on the main thread DispatchQueue.main.async { activityIndicator.stopAnimating() activityIndicator.removeFromSuperview() guard let self = self else { return } if let error = error { self.showAlert(title: "网络错误", message: error.localizedDescription) return } guard let data = data else { self.showAlert(title: "错误", message: "未收到响应数据") return } // 解析JSON响应 // In the response handler for the login request: do { if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { // Check the response code if let code = json["code"] as? String, code == "200" { // Login successful print("登录成功: \(json)") UserDefaults.standard.set(true, forKey: "isLogggedIn") // Check if the token is present in the data if let dataDict = json["data"] as? [String: Any], let token = dataDict["token"] as? String { // Save the token and proceed with login print("Token received: \(token)") UserDefaults.standard.set(token, forKey: "userToken") // Store the token // Navigate to the main interface let mainVC = HomeViewController() let navController = UINavigationController(rootViewController: mainVC) // Replace with your actual main screen controller // self.navigationController?.setViewControllers([mainVC], animated: true) // 获取当前窗口 guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return } // 设置根视图控制器为登录界面 window.rootViewController = navController // 添加切换动画 UIView.transition(with: window, duration: 0.4, options: .transitionCrossDissolve, animations: nil, completion: nil) } else { // Handle missing token error self.showAlert(title: "错误", message: "Token 不存在") } } else { // Log the error message from the server if code isn't 200 print("登录失败: \(json)") if let msg = json["msg"] as? String { print("Login failed with message: \(msg)") self.showAlert(title: "登录失败", message: msg) } else { // Handle case where msg is not returned self.showAlert(title: "登录失败", message: "未知错误") } } } else { // Handle invalid JSON response format self.showAlert(title: "错误", message: "无效的响应格式") } } catch { // Handle JSON parsing errors self.showAlert(title: "解析错误", message: "无法解析响应: \(error.localizedDescription)") } } } task.resume() } func isValidPhoneNumber(_ phoneNumber: String) -> Bool { let phoneRegex = "^1[3-9]\\d{9}$" // This is a basic regex for validating Chinese phone numbers let phoneTest = NSPredicate(format: "SELF MATCHES %@", phoneRegex) return phoneTest.evaluate(with: phoneNumber) } func showAlert(title: String, message: String) { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) present(alert, animated: true, completion: nil) } func startCountdown() { // Disable the button and start the countdown isCountdownActive = true getVerificationCodeButton.isEnabled = false countdownTimer?.invalidate() // Invalidate previous timer if any remainingTime = 60 updateButtonTitle() // Create a new timer to update the button every second countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateCountdown), userInfo: nil, repeats: true) } @objc func updateCountdown() { remainingTime -= 1 updateButtonTitle() if remainingTime <= 0 { // Countdown is complete, reset everything countdownTimer?.invalidate() isCountdownActive = false getVerificationCodeButton.isEnabled = true getVerificationCodeButton.setTitle("获取验证码", for: .normal) } } func updateButtonTitle() { let title = "重新获取 (\(remainingTime)s)" getVerificationCodeButton.setTitle(title, for: .normal) } func showError(_ message: String) { let alert = UIAlertController(title: "提示", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default, handler: nil)) present(alert, animated: true, completion: nil) } @objc func toggleTermsAcceptance() { // Toggle the acceptance of terms isTermsAccepted.toggle() termsCheckboxButton.isSelected = isTermsAccepted } @objc func login() { // Logic to handle login if !isTermsAccepted { showCustomAlert() } else { // Proceed with login if terms are accepted print("登录") loginTapped() } } // MARK: - Custom Alert func showCustomAlert() { // Create a custom alert background (semi-transparent) alertBackground = UIView(frame: view.bounds) alertBackground.backgroundColor = UIColor.black.withAlphaComponent(0.5) alertBackground.translatesAutoresizingMaskIntoConstraints = false view.addSubview(alertBackground) // Create the alert view (popup window) customAlertView = UIView() customAlertView.backgroundColor = .white customAlertView.layer.cornerRadius = 10 customAlertView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(customAlertView) // Add title label let titleLabel = UILabel() titleLabel.text = "温馨提示" titleLabel.font = UIFont.boldSystemFont(ofSize: 18) titleLabel.textAlignment = .center titleLabel.translatesAutoresizingMaskIntoConstraints = false customAlertView.addSubview(titleLabel) // Add message label with colored text let alertFullText = "已阅读并同意《用户协议》和《隐私政策》" let alertAttributedText = NSMutableAttributedString(string: alertFullText) // Set all text to gray first alertAttributedText.addAttribute(.foregroundColor, value: UIColor.gray, range: NSRange(location: 0, length: alertFullText.count)) // Find and set black color for "用户协议" and "隐私政策" let alertRange1 = (alertFullText as NSString).range(of: "用户协议") let alertRange2 = (alertFullText as NSString).range(of: "隐私政策") alertAttributedText.addAttribute(.foregroundColor, value: UIColor.black, range: alertRange1) alertAttributedText.addAttribute(.foregroundColor, value: UIColor.black, range: alertRange2) let messageLabel = UILabel() messageLabel.attributedText = alertAttributedText messageLabel.font = UIFont.systemFont(ofSize: 14) messageLabel.textAlignment = .center messageLabel.numberOfLines = 0 messageLabel.translatesAutoresizingMaskIntoConstraints = false customAlertView.addSubview(messageLabel) // Add Agree Button let agreeButton = UIButton(type: .system) agreeButton.setTitle("同意并登录", for: .normal) agreeButton.backgroundColor = UIColor(hex: "#FFE059") agreeButton.setTitleColor(.black, for: .normal) agreeButton.layer.cornerRadius = 20 agreeButton.addTarget(self, action: #selector(agreeAction), for: .touchUpInside) agreeButton.translatesAutoresizingMaskIntoConstraints = false customAlertView.addSubview(agreeButton) // Add Disagree Button let disagreeButton = UIButton(type: .system) disagreeButton.setTitle("不同意", for: .normal) disagreeButton.setTitleColor(.gray, for: .normal) disagreeButton.addTarget(self, action: #selector(disagreeAction), for: .touchUpInside) disagreeButton.translatesAutoresizingMaskIntoConstraints = false customAlertView.addSubview(disagreeButton) // Set up constraints for alert view NSLayoutConstraint.activate([ // Alert background (full screen, semi-transparent) alertBackground.topAnchor.constraint(equalTo: view.topAnchor), alertBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), alertBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), alertBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor), // Custom alert view (popup) customAlertView.centerXAnchor.constraint(equalTo: view.centerXAnchor), customAlertView.centerYAnchor.constraint(equalTo: view.centerYAnchor), customAlertView.widthAnchor.constraint(equalToConstant: 300), customAlertView.heightAnchor.constraint(equalToConstant: 180), // Title label titleLabel.topAnchor.constraint(equalTo: customAlertView.topAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: customAlertView.leadingAnchor, constant: 20), titleLabel.trailingAnchor.constraint(equalTo: customAlertView.trailingAnchor, constant: -20), // Message label messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), messageLabel.leadingAnchor.constraint(equalTo: customAlertView.leadingAnchor, constant: 20), messageLabel.trailingAnchor.constraint(equalTo: customAlertView.trailingAnchor, constant: -20), // Agree button agreeButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 20), agreeButton.leadingAnchor.constraint(equalTo: customAlertView.leadingAnchor, constant: 20), agreeButton.trailingAnchor.constraint(equalTo: customAlertView.trailingAnchor, constant: -20), agreeButton.heightAnchor.constraint(equalToConstant: 40), // Disagree button disagreeButton.topAnchor.constraint(equalTo: agreeButton.bottomAnchor, constant: 10), disagreeButton.leadingAnchor.constraint(equalTo: customAlertView.leadingAnchor, constant: 20), disagreeButton.trailingAnchor.constraint(equalTo: customAlertView.trailingAnchor, constant: -20), disagreeButton.heightAnchor.constraint(equalToConstant: 40) ]) } @objc func agreeAction() { // Handle the "Agree" action (e.g., proceed with login) print("用户同意协议,继续登录") customAlertView.removeFromSuperview() alertBackground.removeFromSuperview() loginTapped() } @objc func disagreeAction() { // Handle the "Disagree" action (e.g., close the alert) print("用户不同意协议,取消登录") customAlertView.removeFromSuperview() alertBackground.removeFromSuperview() } } extension LoginViewController: UITextViewDelegate { func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { var urlString = "" switch URL.absoluteString { case "terms://service": urlString = "https://ytwljs.github.io/gl-us.html" case "terms://user": urlString = "https://ytwljs.github.io/gl-user.html" case "terms://privacy": urlString = "https://ytwljs.github.io/gl-policy.html" default: return false } let webVC = WebViewController() webVC.urlString = urlString navigationController?.pushViewController(webVC, animated: true) return false } } class WebViewController: UIViewController, WKNavigationDelegate { var urlString: String? var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white webView = WKWebView(frame: view.bounds) webView.navigationDelegate = self webView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(webView) NSLayoutConstraint.activate([ webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) if let urlString = urlString, let url = URL(string: urlString) { let request = URLRequest(url: url) webView.load(request) } } } extension UIColor { convenience init(hex: String) { var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") var rgb: UInt64 = 0 Scanner(string: hexSanitized).scanHexInt64(&rgb) self.init( red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0, blue: CGFloat(rgb & 0x0000FF) / 255.0, alpha: 1.0 ) } }