// // LoginQuestionViewController.swift // RoderickRalph // // Created by AI Assistant on 2025/09/16. // import Foundation import UIKit //import sys/utsname.h class EntryGateController: UIViewController, RoutePickerDelegate { // MARK: - API Endpoints private let pathGetRoutes = "/ditch/list" private let pathSetRoute = "/wx/iosLogin" // MARK: - UI Components private let backdropImage: UIImageView = { let imageView = UIImageView() imageView.image = UIImage(named: "res001") imageView.contentMode = .scaleAspectFill imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private let enterBtn: UIButton = { let button = UIButton(type: .system) button.setTitle("登录", for: .normal) button.setTitleColor(.black, for: .normal) button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 24) button.backgroundColor = .white button.layer.cornerRadius = 25 button.translatesAutoresizingMaskIntoConstraints = false return button }() private let routeBtn: UIButton = { let button = UIButton(type: .system) button.setTitle("渠道 ▼", for: .normal) button.setTitleColor(.black, for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 18) button.backgroundColor = .white button.layer.cornerRadius = 20 button.contentHorizontalAlignment = .center button.translatesAutoresizingMaskIntoConstraints = false return button }() // MARK: - Properties private var routePopup: RoutePickerView? // MARK: - Device Model Helper private func deviceModel() -> String { var systemInfo = utsname() uname(&systemInfo) let machineMirror = Mirror(reflecting: systemInfo.machine) let identifier = machineMirror.children.reduce("") { identifier, element in guard let value = element.value as? Int8, value != 0 else { return identifier } return identifier + String(UnicodeScalar(UInt8(value))) } return deviceNameFor(identifier) } private func deviceNameFor(_ identifier: String) -> String { switch identifier { // iPhone case "iPhone1,1": return "iPhone" case "iPhone1,2": return "iPhone 3G" case "iPhone2,1": return "iPhone 3GS" case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4" case "iPhone4,1": return "iPhone 4S" case "iPhone5,1", "iPhone5,2": return "iPhone 5" case "iPhone5,3", "iPhone5,4": return "iPhone 5c" case "iPhone6,1", "iPhone6,2": return "iPhone 5s" case "iPhone7,2": return "iPhone 6" case "iPhone7,1": return "iPhone 6 Plus" case "iPhone8,1": return "iPhone 6s" case "iPhone8,2": return "iPhone 6s Plus" case "iPhone8,4": return "iPhone SE" case "iPhone9,1", "iPhone9,3": return "iPhone 7" case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus" case "iPhone10,1", "iPhone10,4": return "iPhone 8" case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus" case "iPhone10,3", "iPhone10,6": return "iPhone X" case "iPhone11,2": return "iPhone XS" case "iPhone11,4", "iPhone11,6": return "iPhone XS Max" case "iPhone11,8": return "iPhone XR" case "iPhone12,1": return "iPhone 11" case "iPhone12,3": return "iPhone 11 Pro" case "iPhone12,5": return "iPhone 11 Pro Max" case "iPhone12,8": return "iPhone SE (2nd generation)" case "iPhone13,1": return "iPhone 12 mini" case "iPhone13,2": return "iPhone 12" case "iPhone13,3": return "iPhone 12 Pro" case "iPhone13,4": return "iPhone 12 Pro Max" case "iPhone14,2": return "iPhone 13" case "iPhone14,3": return "iPhone 13 mini" case "iPhone14,4": return "iPhone 13 Pro" case "iPhone14,5": return "iPhone 13 Pro Max" case "iPhone14,6": return "iPhone SE (3rd generation)" case "iPhone14,7": return "iPhone 14" case "iPhone14,8": return "iPhone 14 Plus" case "iPhone15,2": return "iPhone 14 Pro" case "iPhone15,3": return "iPhone 14 Pro Max" case "iPhone15,4": return "iPhone 15" case "iPhone15,5": return "iPhone 15 Plus" case "iPhone16,1": return "iPhone 15 Pro" case "iPhone16,2": return "iPhone 15 Pro Max" case "iPhone17,1": return "iPhone 16 Pro" case "iPhone17,2": return "iPhone 16 Pro Max" case "iPhone17,3": return "iPhone 16" case "iPhone17,4": return "iPhone 16 Plus" case "iPhone17,5": return "iPhone 16e" case "iPhone18,1": return "iPhone 17 Pro" case "iPhone18,2": return "iPhone 17 Pro Max" case "iPhone18,3": return "iPhone 17" case "iPhone18,4": return "iPhone Air" // iPad case "iPad1,1": return "iPad" case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2" case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)" case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)" case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air" case "iPad5,3", "iPad5,4": return "iPad Air 2" case "iPad6,11", "iPad6,12": return "iPad (5th generation)" case "iPad7,5", "iPad7,6": return "iPad (6th generation)" case "iPad7,11", "iPad7,12": return "iPad (7th generation)" case "iPad11,6", "iPad11,7": return "iPad (8th generation)" case "iPad12,1", "iPad12,2": return "iPad (9th generation)" case "iPad13,18", "iPad13,19": return "iPad (10th generation)" case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch)" case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)" case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)" case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)" case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch)" case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)" case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)" case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)" case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)" case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11": return "iPad Pro (12.9-inch) (5th generation)" case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)" case "iPad14,3", "iPad14,4": return "iPad Pro (11-inch) (4th generation)" case "iPad14,5", "iPad14,6": return "iPad Pro (12.9-inch) (6th generation)" case "iPad14,8", "iPad14,9": return "iPad Air (11-inch) (6th generation)" case "iPad14,10", "iPad14,11": return "iPad Air (13-inch) (6th generation)" case "iPad15,3", "iPad15,4": return "iPad Air (11-inch) (7th generation)" case "iPad15,5", "iPad15,6": return "iPad Air (13-inch) (7th generation)" case "iPad15,7", "iPad15,8": return "iPad (11th generation)" case "iPad16,1", "iPad16,2": return "Pad mini (7th generation)" case "iPad16,3", "iPad16,4": return "iPad Pro (11-inch) (5th generation)" case "iPad16,5", "iPad16,6": return "iPad Pro (12.9-inch) (7th generation)" // iPod case "iPod1,1": return "iPod touch" case "iPod2,1": return "iPod touch (2nd generation)" case "iPod3,1": return "iPod touch (3rd generation)" case "iPod4,1": return "iPod touch (4th generation)" case "iPod5,1": return "iPod touch (5th generation)" case "iPod7,1": return "iPod touch (6th generation)" case "iPod9,1": return "iPod touch (7th generation)" // Simulator case "i386", "x86_64", "arm64": return "Simulator" default: return identifier } } private var channels: [Channel] = [] private var selectedChannel: Channel? private var loginUserInfo: LoginUserInfo? // System loading overlay private var loadingOverlay: UIView? private var loadingSpinner: UIActivityIndicatorView? // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() buildUI() applyLayout() wireActions() // Hide navigation bar navigationController?.setNavigationBarHidden(true, animated: false) // Fetch channel list on load fetchRoutes() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) } // MARK: - UI Setup private func buildUI() { view.addSubview(backdropImage) view.addSubview(enterBtn) view.addSubview(routeBtn) } private func applyLayout() { NSLayoutConstraint.activate([ // Background image - fill entire screen backdropImage.topAnchor.constraint(equalTo: view.topAnchor), backdropImage.leadingAnchor.constraint(equalTo: view.leadingAnchor), backdropImage.trailingAnchor.constraint(equalTo: view.trailingAnchor), backdropImage.bottomAnchor.constraint(equalTo: view.bottomAnchor), // Login button - center of screen enterBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), enterBtn.centerYAnchor.constraint(equalTo: view.centerYAnchor,constant: 50), enterBtn.widthAnchor.constraint(equalToConstant: 200), enterBtn.heightAnchor.constraint(equalToConstant: 50), // Channel select button - below login button routeBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), routeBtn.topAnchor.constraint(equalTo: enterBtn.bottomAnchor, constant: 30), routeBtn.widthAnchor.constraint(equalToConstant: 200), routeBtn.heightAnchor.constraint(equalToConstant: 40) ]) } private func wireActions() { enterBtn.addTarget(self, action: #selector(didTapLogin), for: .touchUpInside) routeBtn.addTarget(self, action: #selector(didTapChannelPicker), for: .touchUpInside) } // MARK: - Actions @objc private func didTapLogin() { guard let channel = selectedChannel else { flashToast(message: "请先选择渠道") return } submitRoute(channel: channel) } @objc private func didTapChannelPicker() { guard !channels.isEmpty else { flashToast(message: "渠道列表为空,请稍后重试") fetchRoutes() // Retry fetching return } presentRoutePicker(channels: channels) } // MARK: - Channel Selection Flow private func fetchRoutes() { guard let url = URL(string: apiBaseURL + pathGetRoutes) else { print("[Channel] Invalid getQudaoList URL") return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let bodyParams: [String: Any] = ["appId": kTakuAppID] request.httpBody = try? JSONSerialization.data(withJSONObject: bodyParams, options: []) if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { print("[Channel] Request URL: \(url)") print("[Channel] Request Headers: \(request.allHTTPHeaderFields ?? [:])") print("[Channel] Request Body: \(bodyString)") } let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in if let httpResponse = response as? HTTPURLResponse { print("[Channel] Response Code: \(httpResponse.statusCode)") print("[Channel] Response Headers: \(httpResponse.allHeaderFields)") } if let data = data, let responseString = String(data: data, encoding: .utf8) { print("[Channel] Response Body: \(responseString)") } if let error = error { print("[Channel] fetch error: \(error)") DispatchQueue.main.async { [weak self] in self?.flashToast(message: "网络错误,请重新登录") } return } guard let data = data else { return } let channels = self?.decodeRoutes(from: data) ?? [] DispatchQueue.main.async { if channels.isEmpty { print("[Channel] No channels returned") self?.flashToast(message: "没有渠道信息,请重新登录") } else { self?.channels = channels // Set first channel as default if none selected if self?.selectedChannel == nil && !channels.isEmpty { self?.selectedChannel = channels[0] self?.refreshChannelTitle() } } } } task.resume() } private func decodeRoutes(from data: Data) -> [Channel] { func normalizeArray(_ arr: [Any]) -> [Channel] { var result: [Channel] = [] for (idx, item) in arr.enumerated() { if let dict = item as? [String: Any] { let idAny = dict["id"] ?? dict["ditchId"] ?? dict["value"] ?? idx let nameAny = dict["name"] ?? dict["ditchName"] ?? dict["label"] ?? "\(idAny)" let idStr = String(describing: idAny) let nameStr = String(describing: nameAny) result.append(Channel(id: idStr, name: nameStr)) } else if let str = item as? String { result.append(Channel(id: str, name: str)) } else if let num = item as? NSNumber { let s = num.stringValue result.append(Channel(id: s, name: s)) } } return result } if let obj = try? JSONSerialization.jsonObject(with: data, options: []) { if let dict = obj as? [String: Any] { if let arr = dict["data"] as? [Any] { return normalizeArray(arr) } if let arr = dict["list"] as? [Any] { return normalizeArray(arr) } if let arr = dict["channels"] as? [Any] { return normalizeArray(arr) } } else if let arr = obj as? [Any] { return normalizeArray(arr) } } return [] } private func presentRoutePicker(channels: [Channel]) { routePopup?.removeFromSuperview() let popup = RoutePickerView(channels: channels) popup.delegate = self popup.translatesAutoresizingMaskIntoConstraints = false view.addSubview(popup) NSLayoutConstraint.activate([ popup.topAnchor.constraint(equalTo: view.topAnchor), popup.bottomAnchor.constraint(equalTo: view.bottomAnchor), popup.leadingAnchor.constraint(equalTo: view.leadingAnchor), popup.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) self.routePopup = popup } private func submitRoute(channel: Channel) { guard let url = URL(string: apiBaseURL + pathSetRoute) else { print("[Channel] Invalid setQudao URL") return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") // let userId = String(UserDefaults.standard.object(forKey: "userId") as? Int ?? 0) let memberPhone = UserDefaults.standard.string(forKey: "memberPhone") ?? "" let memberIcon = UserDefaults.standard.string(forKey: "memberIconURL") ?? "" let memberName = UserDefaults.standard.string(forKey: "memberName") ?? memberPhone let phoneInfo: [String: Any] = [ "systemName": UIDevice.current.systemName, "systemVersion": UIDevice.current.systemVersion, "model": deviceModel(), "localizedModel": UIDevice.current.localizedModel ] let phoneJsonString: String if let jsonData = try? JSONSerialization.data(withJSONObject: phoneInfo, options: []), let jsonString = String(data: jsonData, encoding: .utf8) { phoneJsonString = jsonString } else { phoneJsonString = "{}" } let body: [String: Any] = [ "iosId": UIDevice.current.identifierForVendor?.uuidString ?? "", "alias": memberName, "phone": memberPhone, "brand": UIDevice.current.systemName, "model": deviceModel(), "appId": kTakuAppID, "deviceId": UIDevice.current.identifierForVendor?.uuidString ?? "", "iconUrl": memberIcon, "ditchId": Int64(channel.id) ?? 0, "phoneJson": phoneJsonString ] request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { print("[Channel] setQudao Request URL: \(url)") print("[Channel] setQudao Request Headers: \(request.allHTTPHeaderFields ?? [:])") print("[Channel] setQudao Request Body: \(bodyString)") } showSystemOverlay() let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in if let httpResponse = response as? HTTPURLResponse { print("[Channel] setQudao Response Code: \(httpResponse.statusCode)") print("[Channel] setQudao Response Headers: \(httpResponse.allHeaderFields)") } if let data = data, let responseString = String(data: data, encoding: .utf8) { print("[Channel] setQudao Response Body: \(responseString)") } if let data = data { do { if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { if let code = json["code"] as? Int, code != 200 { let message = (json["message"] as? String) ?? (json["msg"] as? String) ?? "未知错误" DispatchQueue.main.async { [weak self] in self?.hideSystemOverlay() self?.flashToast(message: message) } return } } } catch { // Ignore JSON parse error here } } if let data = data, let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let code = obj["code"] as? Int, code == 200, let dataDict = obj["data"] as? [String: Any] { let nick = (dataDict["nickName"] as? String) ?? "" let roleId = (dataDict["userId"] as? String) ?? "" let todayCount: Int = { if let v = dataDict["todayAnswerCount"] as? Int { return v } if let s = dataDict["todayAnswerCount"] as? String, let i = Int(s) { return i } return 0 }() let historyCount: Int = { if let v = dataDict["historyAnswerCount"] as? Int { return v } if let s = dataDict["historyAnswerCount"] as? String, let i = Int(s) { return i } return 0 }() let registryTimeStr = (dataDict["registryTimeStr"] as? String) ?? "" let headImgURL = dataDict["headImg"] as? String let lastLoginTimeStr = (dataDict["lastLoginTimeStr"] as? String) ?? "" let powerValue: Int = { if let v = dataDict["power"] as? Int { return v } if let s = dataDict["power"] as? String, let i = Int(s) { return i } return 0 }() let lastQuestionId: String? = { if let s = dataDict["lastQuestionId"] as? String, !s.isEmpty { return s } if let n = dataDict["lastQuestionId"] as? NSNumber { return n.stringValue } return nil }() let answerLogsFromLogin: [String] = { if let arr = dataDict["answerRecordTimeList"] as? [String] { return arr } if let anyArr = dataDict["answerRecordTimeList"] as? [Any] { return anyArr.compactMap { String(describing: $0) } } return [] }() DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.hideSystemOverlay() self.loginUserInfo = LoginUserInfo( nickName: nick, userId: roleId, registryTimeStr: registryTimeStr, todayAnswerCount: todayCount, historyAnswerCount: historyCount, headImgURL: headImgURL, lastLoginTimeStr: lastLoginTimeStr, answerLogs: answerLogsFromLogin ) // Save to UserDefaults for backup UserDefaults.standard.set(nick, forKey: "nickname") UserDefaults.standard.set(roleId, forKey: "roleID") UserDefaults.standard.set(registryTimeStr, forKey: "registryTimeStr") UserDefaults.standard.set(todayCount, forKey: "todayAnswerCount") UserDefaults.standard.set(historyCount, forKey: "historyAnswerCount") UserDefaults.standard.set(headImgURL, forKey: "headImgURL") UserDefaults.standard.set(lastLoginTimeStr, forKey: "lastLoginTimeStr") UserDefaults.standard.set(powerValue, forKey: "power") UserDefaults.standard.set(answerLogsFromLogin, forKey: "answerLogs") if let lq = lastQuestionId { UserDefaults.standard.set(lq, forKey: "lastQuestionId") } UserDefaults.standard.set(channel.name, forKey: "selectedChannelName") UserDefaults.standard.synchronize() DispatchQueue.main.async { [weak self] in self?.flashToast(message: "登录成功") } // Navigate to Quiz self.goToQuiz() } } if let error = error { print("[Channel] set error: \(error)") DispatchQueue.main.async { [weak self] in self?.hideSystemOverlay() self?.flashToast(message: "网络错误,请重试") } return } DispatchQueue.main.async { self?.hideSystemOverlay() self?.routePopup?.removeFromSuperview() self?.routePopup = nil print("[Channel] setQudao success for: \(channel.name) (id=\(channel.id))") } } task.resume() } private func goToQuiz() { let questionVC = QuizStageController() // 传递登录用户信息 if let userInfo = loginUserInfo { questionVC.setLoginUserInfo(userInfo) } // 传递选择的渠道信息 if let channel = selectedChannel { questionVC.setSelectedChannel(channel) } let navController = UINavigationController(rootViewController: questionVC) if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { if let window = windowScene.windows.first { // 将 questionVC 作为 rootViewController window.rootViewController = navController window.makeKeyAndVisible() } } // Get current window and transition // guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { // return // } // // window.rootViewController = questionVC // // UIView.transition(with: window, // duration: 0.5, // options: .transitionCrossDissolve, // animations: nil, // completion: nil) } private func refreshChannelTitle() { if let channel = selectedChannel { routeBtn.setTitle("\(channel.name) ▼", for: .normal) } else { routeBtn.setTitle("渠道 ▼", for: .normal) } } // MARK: - RoutePickerDelegate func routePicker(_ panel: RoutePickerView, didPick channel: Channel) { selectedChannel = channel refreshChannelTitle() panel.removeFromSuperview() routePopup = nil print("[Channel] Selected channel: \(channel.name) (id=\(channel.id))") } // MARK: - System Loading Helpers private func showSystemOverlay() { guard loadingOverlay == nil else { return } let overlay = UIView() overlay.translatesAutoresizingMaskIntoConstraints = false overlay.backgroundColor = UIColor.black.withAlphaComponent(0.35) overlay.isUserInteractionEnabled = true let spinner = UIActivityIndicatorView(style: .large) spinner.translatesAutoresizingMaskIntoConstraints = false spinner.startAnimating() spinner.hidesWhenStopped = true overlay.addSubview(spinner) view.addSubview(overlay) NSLayoutConstraint.activate([ overlay.topAnchor.constraint(equalTo: view.topAnchor), overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor), overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor), overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor), spinner.centerXAnchor.constraint(equalTo: overlay.centerXAnchor), spinner.centerYAnchor.constraint(equalTo: overlay.centerYAnchor) ]) loadingOverlay = overlay loadingSpinner = spinner } private func hideSystemOverlay() { loadingSpinner?.stopAnimating() loadingOverlay?.removeFromSuperview() loadingSpinner = nil loadingOverlay = nil } // MARK: - Toast Helper private func flashToast(message: String) { let tag = 9527 if let existing = view.viewWithTag(tag) { existing.removeFromSuperview() } let toastLabel = UILabel() toastLabel.text = message toastLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium) toastLabel.textColor = .white toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.8) toastLabel.textAlignment = .center toastLabel.alpha = 0.0 toastLabel.layer.cornerRadius = 16 toastLabel.clipsToBounds = true toastLabel.numberOfLines = 0 toastLabel.tag = tag toastLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(toastLabel) let horizontalPadding: CGFloat = 24 let verticalPadding: CGFloat = 12 let maxWidth = view.frame.width - 40 let size = toastLabel.sizeThatFits(CGSize(width: maxWidth - horizontalPadding * 2, height: CGFloat.greatestFiniteMagnitude)) let width = min(size.width + horizontalPadding * 2, maxWidth) let height = size.height + verticalPadding * 2 NSLayoutConstraint.activate([ toastLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), toastLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), toastLabel.widthAnchor.constraint(equalToConstant: width), toastLabel.heightAnchor.constraint(equalToConstant: height) ]) UIView.animate(withDuration: 0.25, animations: { toastLabel.alpha = 1.0 }) { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { UIView.animate(withDuration: 0.25, animations: { toastLabel.alpha = 0.0 }) { _ in toastLabel.removeFromSuperview() } } } } }