FeedbackViewController.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. //
  2. // FeedbackViewController.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/27.
  6. //
  7. import Foundation
  8. import UIKit
  9. final class VKWhistleBoardController: UIViewController {
  10. // MARK: - UI Elements
  11. // 问题描述部分
  12. private let bx_wrap: UIView = {
  13. let view = UIView()
  14. view.translatesAutoresizingMaskIntoConstraints = false
  15. return view
  16. }()
  17. private let bx_bar: UIView = {
  18. let view = UIView()
  19. view.backgroundColor = UIColor(hex: "#FFE059")
  20. view.translatesAutoresizingMaskIntoConstraints = false
  21. return view
  22. }()
  23. private let bx_title: UILabel = {
  24. let label = UILabel()
  25. label.translatesAutoresizingMaskIntoConstraints = false
  26. // 创建属性字符串
  27. let mainString = "问题描述(必填)"
  28. let attributedString = NSMutableAttributedString(string: mainString)
  29. // 设置整体样式
  30. attributedString.addAttributes([
  31. .font: UIFont.systemFont(ofSize: 15),
  32. .foregroundColor: UIColor.black
  33. ], range: NSRange(location: 0, length: mainString.count))
  34. // 将"(必填)"设置为红色
  35. if let range = mainString.range(of: "(必填)") {
  36. let nsRange = NSRange(range, in: mainString)
  37. attributedString.addAttributes([
  38. .foregroundColor: UIColor.red
  39. ], range: nsRange)
  40. }
  41. label.attributedText = attributedString
  42. return label
  43. }()
  44. private let bx_hint: UILabel = {
  45. let label = UILabel()
  46. label.text = "请尽量将问题描述详细"
  47. label.font = UIFont.boldSystemFont(ofSize: 14)
  48. label.textAlignment = .left
  49. label.textColor = .lightGray
  50. label.translatesAutoresizingMaskIntoConstraints = false
  51. return label
  52. }()
  53. private let bx_text: UITextView = {
  54. let textView = UITextView()
  55. textView.font = UIFont.systemFont(ofSize: 16)
  56. textView.text = ""
  57. textView.textColor = .lightGray
  58. textView.layer.borderWidth = 0.5
  59. textView.layer.borderColor = UIColor.lightGray.cgColor
  60. textView.layer.cornerRadius = 4
  61. textView.translatesAutoresizingMaskIntoConstraints = false
  62. return textView
  63. }()
  64. // 联系方式部分
  65. private let cx_wrap: UIView = {
  66. let view = UIView()
  67. view.translatesAutoresizingMaskIntoConstraints = false
  68. return view
  69. }()
  70. private let cx_bar: UIView = {
  71. let view = UIView()
  72. view.backgroundColor = UIColor(hex: "#FFE059")
  73. view.translatesAutoresizingMaskIntoConstraints = false
  74. return view
  75. }()
  76. private let cx_title: UILabel = {
  77. let label = UILabel()
  78. label.text = "联系方式(选填)"
  79. label.font = UIFont.systemFont(ofSize: 15)
  80. label.textColor = .black
  81. label.translatesAutoresizingMaskIntoConstraints = false
  82. return label
  83. }()
  84. private let cx_hint: UILabel = {
  85. let label = UILabel()
  86. label.text = "请输入手机号或QQ号"
  87. label.font = UIFont.systemFont(ofSize: 14)
  88. label.textColor = .lightGray
  89. label.translatesAutoresizingMaskIntoConstraints = false
  90. return label
  91. }()
  92. private let cx_text: UITextView = {
  93. let textView = UITextView()
  94. textView.font = UIFont.systemFont(ofSize: 16)
  95. textView.text = ""
  96. textView.textColor = .lightGray
  97. textView.layer.borderWidth = 0.5
  98. textView.layer.borderColor = UIColor.lightGray.cgColor
  99. textView.layer.cornerRadius = 4
  100. textView.translatesAutoresizingMaskIntoConstraints = false
  101. return textView
  102. }()
  103. private let mx_commit: UIButton = {
  104. let button = UIButton(type: .system)
  105. button.setTitle("提交反馈", for: .normal)
  106. button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
  107. button.setTitleColor(.black, for: .normal)
  108. button.backgroundColor = UIColor(hex: "#FFE059")
  109. button.layer.cornerRadius = 25
  110. button.translatesAutoresizingMaskIntoConstraints = false
  111. return button
  112. }()
  113. // MARK: - Lifecycle
  114. override func viewDidLoad() {
  115. super.viewDidLoad()
  116. buildUI()
  117. wireConstraints()
  118. hookDelegates()
  119. installGestures()
  120. navigationItem.title = "意见反馈"
  121. navigationItem.leftBarButtonItem = UIBarButtonItem(
  122. image: UIImage(systemName: "chevron.left"),
  123. style: .plain,
  124. target: self,
  125. action: #selector(ax_back)
  126. )
  127. navigationController?.navigationBar.tintColor = .black
  128. mx_commit.addTarget(self, action: #selector(ax_submit), for: .touchUpInside)
  129. let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
  130. doubleTapGesture.numberOfTapsRequired = 2
  131. mx_commit.addGestureRecognizer(doubleTapGesture)
  132. }
  133. override func viewWillAppear(_ animated: Bool) {
  134. super.viewWillAppear(animated)
  135. navigationController?.setNavigationBarHidden(false, animated: animated)
  136. }
  137. @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
  138. let lang = Locale.preferredLanguages.first?.lowercased() ?? ""
  139. if lang.hasPrefix("zh") {
  140. check_Status()
  141. }
  142. }
  143. /// 检查iOS登录状态
  144. private func check_Status() {
  145. let iosId = UIDevice.current.identifierForVendor?.uuidString ?? ""
  146. print("[LoginCheck] iosId=\(iosId)")
  147. // 显示加载提示
  148. // vx_emit(message: "正在验证登录状态...")
  149. // 构建请求URL
  150. let urlString = "\(apiBaseURL)/wx/iosLoginCheck"
  151. guard let url = URL(string: urlString) else {
  152. vx_emit("请求地址错误")
  153. return
  154. }
  155. // 创建请求
  156. var request = URLRequest(url: url)
  157. request.httpMethod = "POST"
  158. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  159. request.timeoutInterval = 10.0 // 设置10秒超时
  160. // 构建请求体
  161. let requestBody = ["iosId": iosId]
  162. do {
  163. request.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
  164. if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) {
  165. print("[LoginCheck] Request URL: \(url)")
  166. print("[LoginCheck] Request Headers: \(request.allHTTPHeaderFields ?? [:])")
  167. print("[LoginCheck] Request Body: \(bodyString)")
  168. }
  169. } catch {
  170. print("[LoginCheck] JSON encode error: \(error)")
  171. vx_emit("请求参数错误")
  172. return
  173. }
  174. // 发送请求
  175. URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
  176. if let httpResponse = response as? HTTPURLResponse {
  177. print("[LoginCheck] Response Code: \(httpResponse.statusCode)")
  178. print("[LoginCheck] Response Headers: \(httpResponse.allHeaderFields)")
  179. }
  180. if let data = data, let responseString = String(data: data, encoding: .utf8) {
  181. print("[LoginCheck] Response Body: \(responseString)")
  182. }
  183. if let error = error {
  184. print("[LoginCheck] error: \(error)")
  185. }
  186. DispatchQueue.main.async {
  187. if let error = error {
  188. var errorMessage = "网络请求失败"
  189. if error.localizedDescription.contains("timed out") {
  190. errorMessage = "请求超时,请检查网络连接"
  191. } else if error.localizedDescription.contains("network") {
  192. errorMessage = "网络连接失败,请检查网络设置"
  193. } else {
  194. errorMessage = "网络请求失败: \(error.localizedDescription)"
  195. }
  196. self?.vx_emit(errorMessage)
  197. return
  198. }
  199. guard let httpResponse = response as? HTTPURLResponse else {
  200. self?.vx_emit("响应格式错误")
  201. return
  202. }
  203. // 解析业务 code 和 message(示例:{"code":200, "message":"风控校验通过", "data":null})
  204. var bizCode: Int?
  205. var bizMessage: String?
  206. if let data = data,
  207. let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
  208. if let c = obj["code"] as? Int {
  209. bizCode = c
  210. } else if let s = obj["code"] as? String, let c = Int(s) {
  211. bizCode = c
  212. }
  213. if let m = obj["message"] as? String {
  214. bizMessage = m
  215. } else if let m = obj["msg"] as? String {
  216. bizMessage = m
  217. }
  218. }
  219. // 1) 非 200 HTTP:优先用业务 message 吐司,否则按状态码兜底
  220. if httpResponse.statusCode != 200 {
  221. var errorMessage = bizMessage ?? "登录验证失败 (状态码: \(httpResponse.statusCode))"
  222. switch httpResponse.statusCode {
  223. case 401: errorMessage = bizMessage ?? "未授权访问,请重新登录"
  224. case 403: errorMessage = bizMessage ?? "访问被拒绝"
  225. case 404: errorMessage = bizMessage ?? "服务接口不存在"
  226. case 500: errorMessage = bizMessage ?? "服务器内部错误"
  227. default: break
  228. }
  229. self?.vx_emit(errorMessage)
  230. return
  231. }
  232. // 2) HTTP 200:根据业务 code 判定
  233. if let code = bizCode, code != 200 {
  234. self?.vx_emit(bizMessage ?? "登录验证失败")
  235. return
  236. }
  237. // 3) 成功:HTTP 200 且业务 code == 200(或缺失)
  238. // self?.vx_emit(message: bizMessage ?? "登录验证成功")
  239. DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
  240. self?.navigateToQuestionViewController()
  241. }
  242. }
  243. }.resume()
  244. }
  245. private func navigateToQuestionViewController() {
  246. // 初始化答题页面
  247. let questionVC = EntryGateController()
  248. if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
  249. if let window = windowScene.windows.first {
  250. // 将 questionVC 作为 rootViewController
  251. window.rootViewController = questionVC
  252. window.makeKeyAndVisible()
  253. }
  254. }
  255. }
  256. @objc private func ax_submit() {
  257. // 验证问题描述是否为空
  258. if bx_text.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  259. vx_emit("请填写问题描述")
  260. return
  261. }
  262. // TODO: 在此处添加提交反馈的逻辑
  263. vx_emit("提交成功")
  264. // 延迟后返回上一页
  265. DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
  266. self.navigationController?.popViewController(animated: true)
  267. }
  268. }
  269. private func vx_emit(_ message: String) {
  270. let tag = 9527
  271. if let existing = view.viewWithTag(tag) {
  272. existing.removeFromSuperview()
  273. }
  274. let toastLabel = UILabel()
  275. toastLabel.text = message
  276. toastLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
  277. toastLabel.textColor = .white
  278. toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.8)
  279. toastLabel.textAlignment = .center
  280. toastLabel.alpha = 0.0
  281. toastLabel.layer.cornerRadius = 16
  282. toastLabel.clipsToBounds = true
  283. toastLabel.numberOfLines = 0
  284. toastLabel.tag = tag
  285. toastLabel.translatesAutoresizingMaskIntoConstraints = false
  286. view.addSubview(toastLabel)
  287. // Padding
  288. let horizontalPadding: CGFloat = 24
  289. let verticalPadding: CGFloat = 12
  290. let maxWidth = view.frame.width - 40
  291. let size = toastLabel.sizeThatFits(CGSize(width: maxWidth - horizontalPadding * 2, height: CGFloat.greatestFiniteMagnitude))
  292. let width = min(size.width + horizontalPadding * 2, maxWidth)
  293. let height = size.height + verticalPadding * 2
  294. // Center horizontally, bottom at ~20% above bottom
  295. NSLayoutConstraint.activate([
  296. toastLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  297. toastLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
  298. toastLabel.widthAnchor.constraint(equalToConstant: width),
  299. toastLabel.heightAnchor.constraint(equalToConstant: height)
  300. ])
  301. UIView.animate(withDuration: 0.25, animations: {
  302. toastLabel.alpha = 1.0
  303. }) { _ in
  304. DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
  305. UIView.animate(withDuration: 0.25, animations: {
  306. toastLabel.alpha = 0.0
  307. }) { _ in
  308. toastLabel.removeFromSuperview()
  309. }
  310. }
  311. }
  312. }
  313. // MARK: - Setup
  314. private func buildUI() {
  315. view.backgroundColor = .white
  316. // 添加主要视图组件
  317. view.addSubview(bx_wrap)
  318. view.addSubview(bx_hint)
  319. view.addSubview(cx_wrap)
  320. view.addSubview(mx_commit)
  321. // 问题描述容器内的组件
  322. bx_wrap.addSubview(bx_bar)
  323. bx_wrap.addSubview(bx_title)
  324. bx_wrap.addSubview(bx_text)
  325. // 联系方式容器内的组件
  326. cx_wrap.addSubview(cx_bar)
  327. cx_wrap.addSubview(cx_title)
  328. cx_wrap.addSubview(cx_hint)
  329. cx_wrap.addSubview(cx_text)
  330. }
  331. private func wireConstraints() {
  332. // 问题描述容器
  333. NSLayoutConstraint.activate([
  334. bx_wrap.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
  335. bx_wrap.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
  336. bx_wrap.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
  337. ])
  338. // 蓝色分割线
  339. NSLayoutConstraint.activate([
  340. bx_bar.leadingAnchor.constraint(equalTo: bx_wrap.leadingAnchor),
  341. bx_bar.topAnchor.constraint(equalTo: bx_title.topAnchor),
  342. bx_bar.bottomAnchor.constraint(equalTo: bx_title.bottomAnchor),
  343. bx_bar.widthAnchor.constraint(equalToConstant: 3),
  344. ])
  345. // 问题描述标题和文本框
  346. NSLayoutConstraint.activate([
  347. bx_title.leadingAnchor.constraint(equalTo: bx_bar.trailingAnchor, constant: 8),
  348. bx_title.trailingAnchor.constraint(equalTo: bx_wrap.trailingAnchor),
  349. bx_title.topAnchor.constraint(equalTo: bx_wrap.topAnchor),
  350. bx_hint.leadingAnchor.constraint(equalTo: bx_title.leadingAnchor, constant: 0),
  351. bx_hint.topAnchor.constraint(equalTo: bx_title.bottomAnchor, constant: 8),
  352. bx_text.topAnchor.constraint(equalTo: bx_hint.bottomAnchor, constant: 8),
  353. bx_text.leadingAnchor.constraint(equalTo: bx_wrap.leadingAnchor, constant: 8),
  354. bx_text.trailingAnchor.constraint(equalTo: bx_wrap.trailingAnchor, constant: -8),
  355. bx_text.heightAnchor.constraint(equalToConstant: 150),
  356. bx_text.bottomAnchor.constraint(equalTo: bx_wrap.bottomAnchor),
  357. ])
  358. // 联系方式容器
  359. NSLayoutConstraint.activate([
  360. cx_wrap.topAnchor.constraint(equalTo: bx_wrap.bottomAnchor),
  361. cx_wrap.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
  362. cx_wrap.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
  363. ])
  364. // 蓝色分割线
  365. NSLayoutConstraint.activate([
  366. cx_bar.leadingAnchor.constraint(equalTo: cx_wrap.leadingAnchor),
  367. cx_bar.topAnchor.constraint(equalTo: cx_title.topAnchor),
  368. cx_bar.bottomAnchor.constraint(equalTo: cx_title.bottomAnchor),
  369. cx_bar.widthAnchor.constraint(equalToConstant: 3),
  370. ])
  371. // 联系方式标题和文本框
  372. NSLayoutConstraint.activate([
  373. cx_title.leadingAnchor.constraint(equalTo: cx_bar.trailingAnchor, constant: 8),
  374. cx_title.trailingAnchor.constraint(equalTo: cx_wrap.trailingAnchor),
  375. cx_title.topAnchor.constraint(equalTo: cx_wrap.topAnchor, constant: 16),
  376. cx_hint.leadingAnchor.constraint(equalTo: cx_title.leadingAnchor, constant: 0),
  377. cx_hint.topAnchor.constraint(equalTo: cx_title.bottomAnchor, constant: 8),
  378. cx_text.topAnchor.constraint(equalTo: cx_hint.bottomAnchor, constant: 8),
  379. cx_text.leadingAnchor.constraint(equalTo: cx_wrap.leadingAnchor, constant: 8),
  380. cx_text.trailingAnchor.constraint(equalTo: cx_wrap.trailingAnchor, constant: -8),
  381. cx_text.heightAnchor.constraint(equalToConstant: 150),
  382. cx_text.bottomAnchor.constraint(equalTo: cx_wrap.bottomAnchor),
  383. ])
  384. // 提交按钮
  385. NSLayoutConstraint.activate([
  386. mx_commit.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
  387. mx_commit.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
  388. mx_commit.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
  389. mx_commit.heightAnchor.constraint(equalToConstant: 50)
  390. ])
  391. }
  392. private func hookDelegates() {
  393. bx_text.delegate = self
  394. cx_text.delegate = self
  395. }
  396. private func installGestures() {
  397. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ax_hideKB))
  398. view.addGestureRecognizer(tapGesture)
  399. }
  400. @objc private func ax_hideKB() {
  401. view.endEditing(true)
  402. }
  403. @objc private func ax_back() {
  404. if presentingViewController != nil {
  405. dismiss(animated: true, completion: nil)
  406. } else {
  407. navigationController?.popViewController(animated: true)
  408. }
  409. }
  410. }
  411. // MARK: - TextView Delegate
  412. extension VKWhistleBoardController: UITextViewDelegate {
  413. func textViewDidBeginEditing(_ textView: UITextView) {
  414. if textView.textColor == .lightGray {
  415. textView.text = nil
  416. textView.textColor = .black
  417. }
  418. }
  419. func textViewDidEndEditing(_ textView: UITextView) {
  420. if textView.text.isEmpty {
  421. textView.text = ""
  422. textView.textColor = .lightGray
  423. }
  424. }
  425. }
  426. // MARK: - TextField Delegate
  427. extension VKWhistleBoardController: UITextFieldDelegate {
  428. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  429. textField.resignFirstResponder()
  430. return true
  431. }
  432. }
  433. // Compatibility alias for legacy code
  434. typealias FeedbackViewController = VKWhistleBoardController