UserSettingsViewController.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. //
  2. // VKNimbusPanelController.swift (obfuscated from UserSettingsViewController)
  3. // VenusKitto
  4. //
  5. // Obfuscated on 2025/08/27
  6. //
  7. import Foundation
  8. import UIKit
  9. // 外部兼容:仍然可以用旧名引用
  10. typealias UserSettingsViewController = VKNimbusPanelController
  11. final class VKNimbusPanelController: UIViewController {
  12. // MARK: - UI (obfuscated names)
  13. private var zxAvatar: UIImage?
  14. private var zxNick: String = "未设置"
  15. private let grid: UITableView = {
  16. let t = UITableView(frame: .zero, style: .grouped)
  17. t.backgroundColor = .white
  18. t.separatorStyle = .none
  19. t.showsVerticalScrollIndicator = false
  20. t.translatesAutoresizingMaskIntoConstraints = false
  21. return t
  22. }()
  23. private let exitBtn: UIButton = {
  24. let b = UIButton(type: .system)
  25. b.setTitle("退出登录", for: .normal)
  26. b.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
  27. b.setTitleColor(.black, for: .normal)
  28. b.backgroundColor = UIColor(hex: "#FFE059")
  29. b.layer.cornerRadius = 25
  30. b.translatesAutoresizingMaskIntoConstraints = false
  31. return b
  32. }()
  33. private let nukeBtn: UIButton = {
  34. let b = UIButton(type: .system)
  35. b.setTitle("注销用户", for: .normal)
  36. b.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
  37. b.setTitleColor(UIColor(hex: "#34260C"), for: .normal)
  38. b.backgroundColor = .clear
  39. b.translatesAutoresizingMaskIntoConstraints = false
  40. return b
  41. }()
  42. // 数据源(标题 + 默认值/占位资源名)
  43. private let entries: [(String, String)] = [
  44. ("头像修改", "Home372"),
  45. ("昵称修改", "未设置")
  46. ]
  47. // MARK: - Lifecycle
  48. override func viewDidLoad() {
  49. super.viewDidLoad()
  50. forgeUI()
  51. pinAutoLayout()
  52. primeTableKit()
  53. bindActions()
  54. navigationItem.title = "用户设置"
  55. navigationController?.navigationBar.tintColor = .black
  56. navigationItem.leftBarButtonItem = UIBarButtonItem(
  57. image: UIImage(systemName: "chevron.left"),
  58. style: .plain,
  59. target: self,
  60. action: #selector(ax_back)
  61. )
  62. }
  63. override func viewWillAppear(_ animated: Bool) {
  64. super.viewWillAppear(animated)
  65. // 头像
  66. if let icon = UserDefaults.standard.string(forKey: "memberIcon"),
  67. !icon.isEmpty, let url = URL(string: icon) {
  68. print("[Settings] pull avatar: \(url.absoluteString)")
  69. DispatchQueue.global().async { [weak self] in
  70. if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
  71. DispatchQueue.main.async { self?.zxAvatar = img; self?.grid.reloadData() }
  72. }
  73. }
  74. } else {
  75. zxAvatar = nil
  76. grid.reloadData()
  77. }
  78. // 昵称
  79. zxNick = UserDefaults.standard.string(forKey: "memberName") ?? "未设置"
  80. grid.reloadData()
  81. navigationController?.setNavigationBarHidden(false, animated: animated)
  82. }
  83. // MARK: - Build UI
  84. private func forgeUI() {
  85. view.backgroundColor = .white
  86. view.addSubview(grid)
  87. view.addSubview(exitBtn)
  88. view.addSubview(nukeBtn)
  89. }
  90. private func pinAutoLayout() {
  91. NSLayoutConstraint.activate([
  92. grid.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
  93. grid.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
  94. grid.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
  95. grid.heightAnchor.constraint(equalToConstant: 150),
  96. nukeBtn.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10),
  97. nukeBtn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
  98. nukeBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
  99. nukeBtn.heightAnchor.constraint(equalToConstant: 50),
  100. exitBtn.bottomAnchor.constraint(equalTo: nukeBtn.topAnchor, constant: -20),
  101. exitBtn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
  102. exitBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
  103. exitBtn.heightAnchor.constraint(equalToConstant: 50)
  104. ])
  105. }
  106. private func primeTableKit() {
  107. grid.register(VXSettingCell.self, forCellReuseIdentifier: VXSettingCell.reuseId)
  108. grid.dataSource = self
  109. grid.delegate = self
  110. grid.isScrollEnabled = false
  111. grid.layer.cornerRadius = 8
  112. }
  113. private func bindActions() {
  114. exitBtn.addTarget(self, action: #selector(ax_logout), for: .touchUpInside)
  115. nukeBtn.addTarget(self, action: #selector(ax_deleteAccount), for: .touchUpInside)
  116. }
  117. // MARK: - Navigation
  118. @objc private func ax_back() {
  119. if presentingViewController != nil { dismiss(animated: true) }
  120. else { navigationController?.popViewController(animated: true) }
  121. }
  122. // MARK: - Actions
  123. @objc private func ax_logout() { askLogout() }
  124. @objc private func ax_deleteAccount() { askDestruct() }
  125. private func askLogout() {
  126. let ac = UIAlertController(title: "退出登录", message: "确定要退出当前账号吗?", preferredStyle: .alert)
  127. ac.addAction(UIAlertAction(title: "取消", style: .cancel))
  128. ac.addAction(UIAlertAction(title: "确定", style: .destructive) { _ in self.doLogout() })
  129. present(ac, animated: true)
  130. }
  131. private func askDestruct() {
  132. let ac = UIAlertController(title: "注销账户", message: "此操作将永久删除您的账户和所有数据,确定要继续吗?", preferredStyle: .alert)
  133. ac.addAction(UIAlertAction(title: "取消", style: .cancel))
  134. ac.addAction(UIAlertAction(title: "注销", style: .destructive) { _ in self.doErase() })
  135. present(ac, animated: true)
  136. }
  137. private func doLogout() {
  138. UserDefaults.standard.set(false, forKey: "isLogggedIn")
  139. UserDefaults.standard.removeObject(forKey: "userToken")
  140. vx_toast("已退出登录")
  141. DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.routeToLogin() }
  142. }
  143. private func doErase() {
  144. vx_toast("账户已注销")
  145. DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.routeToLogin() }
  146. }
  147. private func routeToLogin() {
  148. let loginVC = LoginViewController()
  149. let nav = UINavigationController(rootViewController: loginVC)
  150. guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return }
  151. window.rootViewController = nav
  152. UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: nil)
  153. }
  154. }
  155. // MARK: - Table
  156. extension VKNimbusPanelController: UITableViewDataSource, UITableViewDelegate {
  157. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { entries.count }
  158. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  159. let cell = tableView.dequeueReusableCell(withIdentifier: VXSettingCell.reuseId, for: indexPath) as! VXSettingCell
  160. let (title, val) = entries[indexPath.row]
  161. if indexPath.row == 0 {
  162. cell.bind(title: title,
  163. value: "",
  164. isFirst: indexPath.row == 0,
  165. isLast: indexPath.row == entries.count - 1,
  166. avatar: zxAvatar,
  167. placeholder: val)
  168. } else {
  169. cell.bind(title: title,
  170. value: zxNick,
  171. isFirst: indexPath.row == 0,
  172. isLast: indexPath.row == entries.count - 1,
  173. avatar: nil,
  174. placeholder: val)
  175. }
  176. return cell
  177. }
  178. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { indexPath.row == 0 ? 80 : 60 }
  179. func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { UIView() }
  180. func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 0.1 }
  181. func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { UIView() }
  182. func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 0.1 }
  183. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  184. tableView.deselectRow(at: indexPath, animated: true)
  185. switch indexPath.row {
  186. case 0: popAvatarPanel()
  187. case 1: presentNicknamePad()
  188. default: break
  189. }
  190. }
  191. }
  192. // MARK: - Avatar
  193. extension VKNimbusPanelController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
  194. private func popAvatarPanel() {
  195. let ac = UIAlertController(title: "更换头像", message: nil, preferredStyle: .actionSheet)
  196. ac.modalPresentationStyle = .popover
  197. if let p = ac.popoverPresentationController {
  198. p.delegate = self
  199. let idx = IndexPath(row: 0, section: 0)
  200. if let cell = grid.cellForRow(at: idx) {
  201. p.sourceView = cell.contentView; p.sourceRect = cell.contentView.bounds
  202. } else {
  203. p.sourceView = view; p.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1)
  204. }
  205. p.permittedArrowDirections = []
  206. }
  207. if UIImagePickerController.isSourceTypeAvailable(.camera) {
  208. let a = UIAlertAction(title: "拍照", style: .default) { [weak self] _ in self?.summonCamera() }
  209. a.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(a)
  210. }
  211. let b = UIAlertAction(title: "从相册选择", style: .default) { [weak self] _ in self?.summonLibrary() }
  212. b.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(b)
  213. let c = UIAlertAction(title: "取消", style: .cancel)
  214. c.setValue(UIColor.black, forKey: "titleTextColor"); ac.addAction(c)
  215. present(ac, animated: true)
  216. }
  217. private func summonCamera() {
  218. let p = UIImagePickerController()
  219. p.delegate = self
  220. p.sourceType = .camera
  221. p.allowsEditing = true
  222. present(p, animated: true)
  223. }
  224. private func summonLibrary() {
  225. let p = UIImagePickerController()
  226. p.delegate = self
  227. p.sourceType = .photoLibrary
  228. p.allowsEditing = true
  229. present(p, animated: true)
  230. }
  231. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  232. picker.dismiss(animated: true)
  233. guard let img = info[.editedImage] as? UIImage else { return }
  234. showHud()
  235. guard let data = squash(img) else { hideHud(); vx_toast("图片处理失败"); return }
  236. pushBlob(data) { [weak self] result in
  237. DispatchQueue.main.async {
  238. self?.hideHud()
  239. switch result {
  240. case .success(let url):
  241. self?.zxAvatar = img
  242. self?.grid.reloadData()
  243. self?.syncAvatarUrl(url)
  244. case .failure(let e):
  245. self?.raiseOops("上传失败: \(e.localizedDescription)")
  246. }
  247. }
  248. }
  249. }
  250. func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
  251. if popoverPresentationController.sourceView == nil && popoverPresentationController.barButtonItem == nil {
  252. popoverPresentationController.sourceView = view
  253. popoverPresentationController.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1)
  254. popoverPresentationController.permittedArrowDirections = []
  255. }
  256. }
  257. }
  258. // MARK: - Nickname
  259. extension VKNimbusPanelController {
  260. private func presentNicknamePad() {
  261. let mask = UIView(frame: view.bounds)
  262. mask.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  263. mask.alpha = 0; mask.tag = 1001
  264. view.addSubview(mask)
  265. let panel = UIView(); panel.backgroundColor = .white; panel.layer.cornerRadius = 12; panel.translatesAutoresizingMaskIntoConstraints = false
  266. mask.addSubview(panel)
  267. let title = UILabel(); title.text = "修改昵称"; title.font = .systemFont(ofSize: 16, weight: .medium); title.textAlignment = .center; title.translatesAutoresizingMaskIntoConstraints = false
  268. let tf = UITextField(); tf.placeholder = "请输入昵称"; tf.font = .systemFont(ofSize: 16); tf.borderStyle = .roundedRect; tf.clearButtonMode = .whileEditing; tf.text = (zxNick == "未设置" ? "" : zxNick); tf.translatesAutoresizingMaskIntoConstraints = false
  269. let sep = UIView(); sep.backgroundColor = UIColor(hex: "#EEEEEE"); sep.translatesAutoresizingMaskIntoConstraints = false
  270. let cancel = UIButton(type: .system); cancel.setTitle("取消", for: .normal); cancel.setTitleColor(UIColor(hex: "#999999"), for: .normal); cancel.translatesAutoresizingMaskIntoConstraints = false
  271. let ok = UIButton(type: .system); ok.setTitle("完成", for: .normal); ok.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium); ok.setTitleColor(.black, for: .normal); ok.translatesAutoresizingMaskIntoConstraints = false
  272. panel.addSubview(title); panel.addSubview(tf); panel.addSubview(sep); panel.addSubview(cancel); panel.addSubview(ok)
  273. NSLayoutConstraint.activate([
  274. panel.centerXAnchor.constraint(equalTo: mask.centerXAnchor),
  275. panel.centerYAnchor.constraint(equalTo: mask.centerYAnchor),
  276. panel.widthAnchor.constraint(equalToConstant: 280),
  277. panel.heightAnchor.constraint(equalToConstant: 180),
  278. title.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
  279. title.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 20),
  280. title.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
  281. tf.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 20),
  282. tf.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 20),
  283. tf.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
  284. tf.heightAnchor.constraint(equalToConstant: 40),
  285. sep.topAnchor.constraint(equalTo: tf.bottomAnchor, constant: 20),
  286. sep.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
  287. sep.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
  288. sep.heightAnchor.constraint(equalToConstant: 0.5),
  289. cancel.topAnchor.constraint(equalTo: sep.bottomAnchor),
  290. cancel.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
  291. cancel.trailingAnchor.constraint(equalTo: panel.centerXAnchor),
  292. cancel.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
  293. cancel.heightAnchor.constraint(equalToConstant: 50),
  294. ok.topAnchor.constraint(equalTo: sep.bottomAnchor),
  295. ok.leadingAnchor.constraint(equalTo: panel.centerXAnchor),
  296. ok.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
  297. ok.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
  298. ok.heightAnchor.constraint(equalToConstant: 50)
  299. ])
  300. cancel.addTarget(self, action: #selector(x_dismissOverlay), for: .touchUpInside)
  301. ok.addTarget(self, action: #selector(x_commitNickname), for: .touchUpInside)
  302. mask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(x_dismissOverlay)))
  303. UIView.animate(withDuration: 0.3) { mask.alpha = 1 }
  304. tf.becomeFirstResponder()
  305. }
  306. @objc private func x_dismissOverlay() {
  307. if let mask = view.viewWithTag(1001) {
  308. UIView.animate(withDuration: 0.2, animations: { mask.alpha = 0 }) { _ in mask.removeFromSuperview() }
  309. }
  310. }
  311. @objc private func x_commitNickname() {
  312. guard let mask = view.viewWithTag(1001),
  313. let panel = mask.subviews.first,
  314. let tf = panel.subviews.first(where: { $0 is UITextField }) as? UITextField,
  315. let text = tf.text, !text.isEmpty else {
  316. vx_toast("昵称不能为空")
  317. return
  318. }
  319. rx_updateNick(text)
  320. x_dismissOverlay()
  321. }
  322. }
  323. // MARK: - Networking helpers
  324. extension VKNimbusPanelController {
  325. private func squash(_ image: UIImage) -> Data? {
  326. let target = CGSize(width: 800, height: 800)
  327. return image.x_resize(to: target)?.jpegData(compressionQuality: 0.7)
  328. }
  329. private func showHud() {
  330. let indicator = UIActivityIndicatorView(style: .large)
  331. indicator.color = .white
  332. indicator.startAnimating()
  333. let bg = UIView(frame: view.bounds)
  334. bg.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  335. bg.tag = 1002
  336. bg.addSubview(indicator)
  337. indicator.center = bg.center
  338. view.addSubview(bg)
  339. }
  340. private func hideHud() { view.viewWithTag(1002)?.removeFromSuperview() }
  341. private func pushBlob(_ imageData: Data, completion: @escaping (Result<String, Error>) -> Void) {
  342. guard let token = UserDefaults.standard.string(forKey: "userToken") else {
  343. completion(.failure(NSError(domain: "AuthError", code: 401, userInfo: [NSLocalizedDescriptionKey: "未找到用户凭证"])))
  344. return
  345. }
  346. guard let url = URL(string: "\(baseURL)/common/upload") else {
  347. completion(.failure(NSError(domain: "URLError", code: 400, userInfo: [NSLocalizedDescriptionKey: "无效的服务器地址"])))
  348. return
  349. }
  350. var req = URLRequest(url: url)
  351. req.httpMethod = "POST"
  352. req.setValue(token, forHTTPHeaderField: "Authorization")
  353. let boundary = "Boundary-\(UUID().uuidString)"
  354. req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
  355. var body = Data()
  356. body.append("--\(boundary)\r\n".data(using: .utf8)!)
  357. body.append("Content-Disposition: form-data; name=\"file\"; filename=\"avatar.jpg\"\r\n".data(using: .utf8)!)
  358. body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
  359. body.append(imageData)
  360. body.append("\r\n".data(using: .utf8)!)
  361. body.append("--\(boundary)--\r\n".data(using: .utf8)!)
  362. req.httpBody = body
  363. dbg_emitRequest(name: "上传头像(文件)", request: req, body: body)
  364. URLSession.shared.dataTask(with: req) { [weak self] data, response, error in
  365. self?.dbg_emitResponse(name: "上传头像(文件)", data: data, response: response, error: error)
  366. if let error = error { completion(.failure(error)); return }
  367. guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else {
  368. completion(.failure(NSError(domain: "ServerError", code: 500, userInfo: [NSLocalizedDescriptionKey: "服务器返回错误"])))
  369. return
  370. }
  371. do {
  372. if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
  373. let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }),
  374. codeStr == "200",
  375. let dataDict = json["data"] as? [String: Any],
  376. let imageUrl = dataDict["url"] as? String {
  377. completion(.success(imageUrl))
  378. } else {
  379. completion(.failure(NSError(domain: "ParseError", code: 500, userInfo: [NSLocalizedDescriptionKey: "响应解析失败"])))
  380. }
  381. } catch {
  382. completion(.failure(error))
  383. }
  384. }.resume()
  385. }
  386. private func syncAvatarUrl(_ imageUrl: String) {
  387. guard let token = UserDefaults.standard.string(forKey: "userToken") else { raiseOops("未找到用户凭证"); return }
  388. guard let url = URL(string: "\(baseURL)/petRecordApUser/modifyMemberIcon") else { raiseOops("无效的服务器地址"); return }
  389. var req = URLRequest(url: url)
  390. req.httpMethod = "POST"
  391. req.setValue("application/json", forHTTPHeaderField: "Content-Type")
  392. req.setValue(token, forHTTPHeaderField: "Authorization")
  393. let payload: [String: Any] = ["memberIcon": imageUrl]
  394. do {
  395. req.httpBody = try JSONSerialization.data(withJSONObject: payload)
  396. dbg_emitRequest(name: "更新头像URL", request: req, body: req.httpBody)
  397. } catch { raiseOops("请求创建失败"); return }
  398. showHud()
  399. URLSession.shared.dataTask(with: req) { [weak self] data, response, error in
  400. DispatchQueue.main.async {
  401. self?.dbg_emitResponse(name: "更新头像URL", data: data, response: response, error: error)
  402. self?.hideHud()
  403. if let error = error { self?.raiseOops("更新失败: \(error.localizedDescription)"); return }
  404. guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else { self?.raiseOops("服务器返回错误"); return }
  405. do {
  406. if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
  407. let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }),
  408. codeStr == "200" {
  409. self?.vx_toast("头像更新成功")
  410. UserDefaults.standard.set(imageUrl, forKey: "memberIcon")
  411. NotificationCenter.default.post(name: Notification.Name("UserAvatarUpdated"), object: imageUrl)
  412. } else {
  413. self?.raiseOops("更新失败")
  414. }
  415. } catch {
  416. self?.raiseOops("响应解析失败")
  417. }
  418. }
  419. }.resume()
  420. }
  421. private func rx_updateNick(_ newNickname: String) {
  422. guard let token = UserDefaults.standard.string(forKey: "userToken") else { raiseOops("未找到用户凭证"); return }
  423. guard let url = URL(string: "\(baseURL)/petRecordApUser/modifyMemberName") else { raiseOops("无效的服务器地址"); return }
  424. var req = URLRequest(url: url)
  425. req.httpMethod = "POST"
  426. req.setValue("application/json", forHTTPHeaderField: "Content-Type")
  427. req.setValue(token, forHTTPHeaderField: "Authorization")
  428. let payload: [String: Any] = ["memberName": newNickname]
  429. do {
  430. req.httpBody = try JSONSerialization.data(withJSONObject: payload)
  431. dbg_emitRequest(name: "修改昵称", request: req, body: req.httpBody)
  432. } catch { raiseOops("请求创建失败"); return }
  433. showHud()
  434. URLSession.shared.dataTask(with: req) { [weak self] data, response, error in
  435. DispatchQueue.main.async {
  436. self?.dbg_emitResponse(name: "修改昵称", data: data, response: response, error: error)
  437. self?.hideHud()
  438. if let error = error { self?.raiseOops("更新失败: \(error.localizedDescription)"); return }
  439. guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode), let data = data else { self?.raiseOops("服务器返回错误"); return }
  440. do {
  441. if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
  442. let codeStr: String = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }),
  443. codeStr == "200" {
  444. self?.zxNick = newNickname
  445. UserDefaults.standard.set(newNickname, forKey: "memberName")
  446. NotificationCenter.default.post(name: Notification.Name("UserNicknameUpdated"), object: newNickname)
  447. self?.grid.reloadData()
  448. self?.vx_toast("昵称修改成功")
  449. } else {
  450. self?.raiseOops("更新失败")
  451. }
  452. } catch {
  453. self?.raiseOops("响应解析失败")
  454. }
  455. }
  456. }.resume()
  457. }
  458. }
  459. // MARK: - Cell (obfuscated)
  460. final class VXSettingCell: UITableViewCell {
  461. static let reuseId = "vx.setting"
  462. private let tLabel: UILabel = {
  463. let l = UILabel()
  464. l.font = .systemFont(ofSize: 16, weight: .medium)
  465. l.textColor = .gray
  466. l.translatesAutoresizingMaskIntoConstraints = false
  467. return l
  468. }()
  469. private let vLabel: UILabel = {
  470. let l = UILabel()
  471. l.font = .systemFont(ofSize: 16, weight: .medium)
  472. l.textColor = UIColor(hex: "#999999")
  473. l.translatesAutoresizingMaskIntoConstraints = false
  474. return l
  475. }()
  476. private let head: UIImageView = {
  477. let iv = UIImageView()
  478. iv.contentMode = .scaleAspectFill
  479. iv.layer.cornerRadius = 25
  480. iv.clipsToBounds = true
  481. iv.backgroundColor = UIColor(hex: "#F0F0F0")
  482. iv.translatesAutoresizingMaskIntoConstraints = false
  483. return iv
  484. }()
  485. private let arrow: UIImageView = {
  486. let iv = UIImageView()
  487. iv.image = UIImage(systemName: "chevron.right")
  488. iv.tintColor = UIColor(hex: "#000000")
  489. iv.translatesAutoresizingMaskIntoConstraints = false
  490. return iv
  491. }()
  492. private let line: UIView = {
  493. let v = UIView()
  494. v.backgroundColor = UIColor(hex: "#EEEEEE")
  495. v.translatesAutoresizingMaskIntoConstraints = false
  496. return v
  497. }()
  498. override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
  499. super.init(style: style, reuseIdentifier: reuseIdentifier)
  500. contentView.backgroundColor = .white
  501. selectionStyle = .none
  502. contentView.addSubview(tLabel)
  503. contentView.addSubview(vLabel)
  504. contentView.addSubview(arrow)
  505. contentView.addSubview(line)
  506. contentView.addSubview(head)
  507. NSLayoutConstraint.activate([
  508. tLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
  509. tLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  510. arrow.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
  511. arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  512. arrow.widthAnchor.constraint(equalToConstant: 12),
  513. arrow.heightAnchor.constraint(equalToConstant: 18),
  514. vLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  515. vLabel.trailingAnchor.constraint(equalTo: arrow.leadingAnchor, constant: -8),
  516. head.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
  517. head.trailingAnchor.constraint(equalTo: arrow.leadingAnchor, constant: -8),
  518. head.widthAnchor.constraint(equalToConstant: 50),
  519. head.heightAnchor.constraint(equalToConstant: 50),
  520. line.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
  521. line.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
  522. line.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
  523. line.heightAnchor.constraint(equalToConstant: 0.5)
  524. ])
  525. }
  526. required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
  527. func bind(title: String, value: String, isFirst: Bool, isLast: Bool, avatar: UIImage?, placeholder: String) {
  528. tLabel.text = title
  529. vLabel.isHidden = true
  530. head.isHidden = true
  531. if title == "头像修改" {
  532. head.isHidden = false
  533. head.image = avatar ?? UIImage(named: placeholder) ?? UIImage(named: "Home372")
  534. } else {
  535. vLabel.isHidden = false
  536. vLabel.text = value
  537. }
  538. line.isHidden = isLast
  539. }
  540. }
  541. // MARK: - Utils
  542. extension VKNimbusPanelController {
  543. fileprivate func vx_toast(_ message: String) {
  544. let lab = UILabel()
  545. lab.text = message
  546. lab.font = .systemFont(ofSize: 14)
  547. lab.textColor = .white
  548. lab.backgroundColor = UIColor.black.withAlphaComponent(0.7)
  549. lab.textAlignment = .center
  550. lab.alpha = 0
  551. lab.layer.cornerRadius = 8
  552. lab.clipsToBounds = true
  553. lab.translatesAutoresizingMaskIntoConstraints = false
  554. view.addSubview(lab)
  555. NSLayoutConstraint.activate([
  556. lab.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  557. lab.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100),
  558. lab.widthAnchor.constraint(equalToConstant: 160),
  559. lab.heightAnchor.constraint(equalToConstant: 40)
  560. ])
  561. UIView.animate(withDuration: 0.3, animations: { lab.alpha = 1 }) { _ in
  562. DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
  563. UIView.animate(withDuration: 0.3, animations: { lab.alpha = 0 }) { _ in lab.removeFromSuperview() }
  564. }
  565. }
  566. }
  567. fileprivate func raiseOops(_ msg: String) {
  568. let ac = UIAlertController(title: "错误", message: msg, preferredStyle: .alert)
  569. ac.addAction(UIAlertAction(title: "确定", style: .default))
  570. present(ac, animated: true)
  571. }
  572. }
  573. // MARK: - UIImage resize
  574. extension UIImage {
  575. fileprivate func x_resize(to size: CGSize) -> UIImage? {
  576. UIGraphicsBeginImageContextWithOptions(size, false, scale)
  577. defer { UIGraphicsEndImageContext() }
  578. draw(in: CGRect(origin: .zero, size: size))
  579. return UIGraphicsGetImageFromCurrentImageContext()
  580. }
  581. }
  582. // MARK: - Debug loggers (obfuscated names)
  583. extension VKNimbusPanelController {
  584. func dbg_emitRequest(name: String, request: URLRequest, body: Data?) {
  585. #if DEBUG
  586. var lines: [String] = []
  587. lines.append("\n====================[REQUEST: \(name)]====================")
  588. lines.append("URL : \(request.url?.absoluteString ?? "-")")
  589. lines.append("Method : \(request.httpMethod ?? "GET")")
  590. lines.append("Headers : \(request.allHTTPHeaderFields ?? [:])")
  591. if let ct = request.value(forHTTPHeaderField: "Content-Type"), ct.contains("multipart/form-data"), let body = body {
  592. lines.append("Body : <multipart> size=\(body.count) bytes")
  593. } else if let body = body, !body.isEmpty {
  594. if let pretty = dbg_pretty(body) { lines.append("Body(JSON): \n\(pretty)") }
  595. else if let s = String(data: body, encoding: .utf8) { lines.append("Body(Text): \n\(s)") }
  596. else { lines.append("Body(Binary) size=\(body.count) bytes") }
  597. } else {
  598. lines.append("Body : <empty>")
  599. }
  600. lines.append("cURL : \n\(dbg_curl(from: request, body: body))")
  601. lines.append("==========================================================\n")
  602. print(lines.joined(separator: "\n"))
  603. #endif
  604. }
  605. func dbg_emitResponse(name: String, data: Data?, response: URLResponse?, error: Error?) {
  606. #if DEBUG
  607. var lines: [String] = []
  608. lines.append("\n--------------------[RESPONSE: \(name)]--------------------")
  609. if let http = response as? HTTPURLResponse {
  610. lines.append("Status : \(http.statusCode)")
  611. lines.append("URL : \(http.url?.absoluteString ?? "-")")
  612. lines.append("Headers : \(http.allHeaderFields)")
  613. }
  614. if let error = error { lines.append("Error : \(error.localizedDescription)") }
  615. if let d = data, !d.isEmpty {
  616. if let pretty = dbg_pretty(d) { lines.append("Body(JSON): \n\(pretty)") }
  617. else if let s = String(data: d, encoding: .utf8) { lines.append("Body(Text): \n\(s)") }
  618. else { lines.append("Body(Binary) size=\(d.count) bytes") }
  619. } else { lines.append("Body : <empty>") }
  620. lines.append("----------------------------------------------------------\n")
  621. print(lines.joined(separator: "\n"))
  622. #endif
  623. }
  624. private func dbg_pretty(_ data: Data) -> String? {
  625. if let obj = try? JSONSerialization.jsonObject(with: data),
  626. let json = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]),
  627. let s = String(data: json, encoding: .utf8) { return s }
  628. return nil
  629. }
  630. private func dbg_curl(from request: URLRequest, body: Data?) -> String {
  631. var parts: [String] = ["curl -i"]
  632. if let m = request.httpMethod { parts.append("-X \(m)") }
  633. if let headers = request.allHTTPHeaderFields {
  634. for (k, v) in headers { parts.append("-H '\(k): \(v)'") }
  635. }
  636. if let body = body, !body.isEmpty, !(request.value(forHTTPHeaderField: "Content-Type")?.contains("multipart/form-data") ?? false) {
  637. if let s = String(data: body, encoding: .utf8) {
  638. let esc = s.replacingOccurrences(of: "'", with: "'\\''")
  639. parts.append("--data '\(esc)'")
  640. }
  641. }
  642. if let u = request.url?.absoluteString { parts.append("'\(u)'") }
  643. return parts.joined(separator: " ")
  644. }
  645. }