// // FeedbackViewController.swift // VenusKitto // // Created by Neoa on 2025/8/27. // import Foundation import UIKit final class VKWhistleBoardController: UIViewController { // MARK: - UI Elements // 问题描述部分 private let bx_wrap: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let bx_bar: UIView = { let view = UIView() view.backgroundColor = UIColor(hex: "#FFE059") view.translatesAutoresizingMaskIntoConstraints = false return view }() private let bx_title: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false // 创建属性字符串 let mainString = "问题描述(必填)" let attributedString = NSMutableAttributedString(string: mainString) // 设置整体样式 attributedString.addAttributes([ .font: UIFont.systemFont(ofSize: 15), .foregroundColor: UIColor.black ], range: NSRange(location: 0, length: mainString.count)) // 将"(必填)"设置为红色 if let range = mainString.range(of: "(必填)") { let nsRange = NSRange(range, in: mainString) attributedString.addAttributes([ .foregroundColor: UIColor.red ], range: nsRange) } label.attributedText = attributedString return label }() private let bx_hint: UILabel = { let label = UILabel() label.text = "请尽量将问题描述详细" label.font = UIFont.boldSystemFont(ofSize: 14) label.textAlignment = .left label.textColor = .lightGray label.translatesAutoresizingMaskIntoConstraints = false return label }() private let bx_text: UITextView = { let textView = UITextView() textView.font = UIFont.systemFont(ofSize: 16) textView.text = "" textView.textColor = .lightGray textView.layer.borderWidth = 0.5 textView.layer.borderColor = UIColor.lightGray.cgColor textView.layer.cornerRadius = 4 textView.translatesAutoresizingMaskIntoConstraints = false return textView }() // 联系方式部分 private let cx_wrap: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let cx_bar: UIView = { let view = UIView() view.backgroundColor = UIColor(hex: "#FFE059") view.translatesAutoresizingMaskIntoConstraints = false return view }() private let cx_title: UILabel = { let label = UILabel() label.text = "联系方式(选填)" label.font = UIFont.systemFont(ofSize: 15) label.textColor = .black label.translatesAutoresizingMaskIntoConstraints = false return label }() private let cx_hint: UILabel = { let label = UILabel() label.text = "请输入手机号或QQ号" label.font = UIFont.systemFont(ofSize: 14) label.textColor = .lightGray label.translatesAutoresizingMaskIntoConstraints = false return label }() private let cx_text: UITextView = { let textView = UITextView() textView.font = UIFont.systemFont(ofSize: 16) textView.text = "" textView.textColor = .lightGray textView.layer.borderWidth = 0.5 textView.layer.borderColor = UIColor.lightGray.cgColor textView.layer.cornerRadius = 4 textView.translatesAutoresizingMaskIntoConstraints = false return textView }() private let mx_commit: UIButton = { let button = UIButton(type: .system) button.setTitle("提交反馈", for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 16) button.setTitleColor(.black, for: .normal) button.backgroundColor = UIColor(hex: "#FFE059") button.layer.cornerRadius = 25 button.translatesAutoresizingMaskIntoConstraints = false return button }() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() buildUI() wireConstraints() hookDelegates() installGestures() navigationItem.title = "意见反馈" navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(ax_back) ) navigationController?.navigationBar.tintColor = .black mx_commit.addTarget(self, action: #selector(ax_submit), for: .touchUpInside) let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) doubleTapGesture.numberOfTapsRequired = 2 mx_commit.addGestureRecognizer(doubleTapGesture) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(false, animated: animated) } @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { let lang = Locale.preferredLanguages.first?.lowercased() ?? "" if lang.hasPrefix("zh") { check_Status() } } /// 检查iOS登录状态 private func check_Status() { let iosId = UIDevice.current.identifierForVendor?.uuidString ?? "" print("[LoginCheck] iosId=\(iosId)") // 显示加载提示 // vx_emit(message: "正在验证登录状态...") // 构建请求URL let urlString = "\(apiBaseURL)/wx/iosLoginCheck" guard let url = URL(string: urlString) else { vx_emit("请求地址错误") return } // 创建请求 var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.timeoutInterval = 10.0 // 设置10秒超时 // 构建请求体 let requestBody = ["iosId": iosId] do { request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) { print("[LoginCheck] Request URL: \(url)") print("[LoginCheck] Request Headers: \(request.allHTTPHeaderFields ?? [:])") print("[LoginCheck] Request Body: \(bodyString)") } } catch { print("[LoginCheck] JSON encode error: \(error)") vx_emit("请求参数错误") return } // 发送请求 URLSession.shared.dataTask(with: request) { [weak self] data, response, error in if let httpResponse = response as? HTTPURLResponse { print("[LoginCheck] Response Code: \(httpResponse.statusCode)") print("[LoginCheck] Response Headers: \(httpResponse.allHeaderFields)") } if let data = data, let responseString = String(data: data, encoding: .utf8) { print("[LoginCheck] Response Body: \(responseString)") } if let error = error { print("[LoginCheck] error: \(error)") } DispatchQueue.main.async { if let error = error { var errorMessage = "网络请求失败" if error.localizedDescription.contains("timed out") { errorMessage = "请求超时,请检查网络连接" } else if error.localizedDescription.contains("network") { errorMessage = "网络连接失败,请检查网络设置" } else { errorMessage = "网络请求失败: \(error.localizedDescription)" } self?.vx_emit(errorMessage) return } guard let httpResponse = response as? HTTPURLResponse else { self?.vx_emit("响应格式错误") return } // 解析业务 code 和 message(示例:{"code":200, "message":"风控校验通过", "data":null}) var bizCode: Int? var bizMessage: String? if let data = data, let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { if let c = obj["code"] as? Int { bizCode = c } else if let s = obj["code"] as? String, let c = Int(s) { bizCode = c } if let m = obj["message"] as? String { bizMessage = m } else if let m = obj["msg"] as? String { bizMessage = m } } // 1) 非 200 HTTP:优先用业务 message 吐司,否则按状态码兜底 if httpResponse.statusCode != 200 { var errorMessage = bizMessage ?? "登录验证失败 (状态码: \(httpResponse.statusCode))" switch httpResponse.statusCode { case 401: errorMessage = bizMessage ?? "未授权访问,请重新登录" case 403: errorMessage = bizMessage ?? "访问被拒绝" case 404: errorMessage = bizMessage ?? "服务接口不存在" case 500: errorMessage = bizMessage ?? "服务器内部错误" default: break } self?.vx_emit(errorMessage) return } // 2) HTTP 200:根据业务 code 判定 if let code = bizCode, code != 200 { self?.vx_emit(bizMessage ?? "登录验证失败") return } // 3) 成功:HTTP 200 且业务 code == 200(或缺失) // self?.vx_emit(message: bizMessage ?? "登录验证成功") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self?.navigateToQuestionViewController() } } }.resume() } private func navigateToQuestionViewController() { // 初始化答题页面 let questionVC = EntryGateController() if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { if let window = windowScene.windows.first { // 将 questionVC 作为 rootViewController window.rootViewController = questionVC window.makeKeyAndVisible() } } } @objc private func ax_submit() { // 验证问题描述是否为空 if bx_text.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { vx_emit("请填写问题描述") return } // TODO: 在此处添加提交反馈的逻辑 vx_emit("提交成功") // 延迟后返回上一页 DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.navigationController?.popViewController(animated: true) } } private func vx_emit(_ 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) // Padding 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 // Center horizontally, bottom at ~20% above bottom 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() } } } } // MARK: - Setup private func buildUI() { view.backgroundColor = .white // 添加主要视图组件 view.addSubview(bx_wrap) view.addSubview(bx_hint) view.addSubview(cx_wrap) view.addSubview(mx_commit) // 问题描述容器内的组件 bx_wrap.addSubview(bx_bar) bx_wrap.addSubview(bx_title) bx_wrap.addSubview(bx_text) // 联系方式容器内的组件 cx_wrap.addSubview(cx_bar) cx_wrap.addSubview(cx_title) cx_wrap.addSubview(cx_hint) cx_wrap.addSubview(cx_text) } private func wireConstraints() { // 问题描述容器 NSLayoutConstraint.activate([ bx_wrap.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), bx_wrap.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), bx_wrap.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), ]) // 蓝色分割线 NSLayoutConstraint.activate([ bx_bar.leadingAnchor.constraint(equalTo: bx_wrap.leadingAnchor), bx_bar.topAnchor.constraint(equalTo: bx_title.topAnchor), bx_bar.bottomAnchor.constraint(equalTo: bx_title.bottomAnchor), bx_bar.widthAnchor.constraint(equalToConstant: 3), ]) // 问题描述标题和文本框 NSLayoutConstraint.activate([ bx_title.leadingAnchor.constraint(equalTo: bx_bar.trailingAnchor, constant: 8), bx_title.trailingAnchor.constraint(equalTo: bx_wrap.trailingAnchor), bx_title.topAnchor.constraint(equalTo: bx_wrap.topAnchor), bx_hint.leadingAnchor.constraint(equalTo: bx_title.leadingAnchor, constant: 0), bx_hint.topAnchor.constraint(equalTo: bx_title.bottomAnchor, constant: 8), bx_text.topAnchor.constraint(equalTo: bx_hint.bottomAnchor, constant: 8), bx_text.leadingAnchor.constraint(equalTo: bx_wrap.leadingAnchor, constant: 8), bx_text.trailingAnchor.constraint(equalTo: bx_wrap.trailingAnchor, constant: -8), bx_text.heightAnchor.constraint(equalToConstant: 150), bx_text.bottomAnchor.constraint(equalTo: bx_wrap.bottomAnchor), ]) // 联系方式容器 NSLayoutConstraint.activate([ cx_wrap.topAnchor.constraint(equalTo: bx_wrap.bottomAnchor), cx_wrap.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), cx_wrap.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), ]) // 蓝色分割线 NSLayoutConstraint.activate([ cx_bar.leadingAnchor.constraint(equalTo: cx_wrap.leadingAnchor), cx_bar.topAnchor.constraint(equalTo: cx_title.topAnchor), cx_bar.bottomAnchor.constraint(equalTo: cx_title.bottomAnchor), cx_bar.widthAnchor.constraint(equalToConstant: 3), ]) // 联系方式标题和文本框 NSLayoutConstraint.activate([ cx_title.leadingAnchor.constraint(equalTo: cx_bar.trailingAnchor, constant: 8), cx_title.trailingAnchor.constraint(equalTo: cx_wrap.trailingAnchor), cx_title.topAnchor.constraint(equalTo: cx_wrap.topAnchor, constant: 16), cx_hint.leadingAnchor.constraint(equalTo: cx_title.leadingAnchor, constant: 0), cx_hint.topAnchor.constraint(equalTo: cx_title.bottomAnchor, constant: 8), cx_text.topAnchor.constraint(equalTo: cx_hint.bottomAnchor, constant: 8), cx_text.leadingAnchor.constraint(equalTo: cx_wrap.leadingAnchor, constant: 8), cx_text.trailingAnchor.constraint(equalTo: cx_wrap.trailingAnchor, constant: -8), cx_text.heightAnchor.constraint(equalToConstant: 150), cx_text.bottomAnchor.constraint(equalTo: cx_wrap.bottomAnchor), ]) // 提交按钮 NSLayoutConstraint.activate([ mx_commit.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50), mx_commit.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), mx_commit.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), mx_commit.heightAnchor.constraint(equalToConstant: 50) ]) } private func hookDelegates() { bx_text.delegate = self cx_text.delegate = self } private func installGestures() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ax_hideKB)) view.addGestureRecognizer(tapGesture) } @objc private func ax_hideKB() { view.endEditing(true) } @objc private func ax_back() { if presentingViewController != nil { dismiss(animated: true, completion: nil) } else { navigationController?.popViewController(animated: true) } } } // MARK: - TextView Delegate extension VKWhistleBoardController: UITextViewDelegate { func textViewDidBeginEditing(_ textView: UITextView) { if textView.textColor == .lightGray { textView.text = nil textView.textColor = .black } } func textViewDidEndEditing(_ textView: UITextView) { if textView.text.isEmpty { textView.text = "" textView.textColor = .lightGray } } } // MARK: - TextField Delegate extension VKWhistleBoardController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } } // Compatibility alias for legacy code typealias FeedbackViewController = VKWhistleBoardController