MyPetsViewController.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. //
  2. // MyPetsViewController.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/27.
  6. //
  7. import Foundation
  8. import UIKit
  9. // MARK: - FlexibleCode (string or int)
  10. fileprivate struct FlexibleCode: Codable {
  11. let raw: String
  12. init(from decoder: Decoder) throws {
  13. let c = try decoder.singleValueContainer()
  14. if let s = try? c.decode(String.self) { raw = s }
  15. else if let i = try? c.decode(Int.self) { raw = String(i) }
  16. else { raw = "" }
  17. }
  18. }
  19. // MARK: - 我的宠物 列表页
  20. final class MyPetsViewController: UIViewController {
  21. // UI
  22. private let titleLabel: UILabel = {
  23. let l = UILabel()
  24. l.text = "我的宠物"
  25. l.font = .systemFont(ofSize: 28, weight: .semibold)
  26. l.textColor = UIColor(hex: "#2B2B2B")
  27. return l
  28. }()
  29. private let scrollView = UIScrollView()
  30. private let contentView = UIView()
  31. private let stack = UIStackView()
  32. private let imageCache = NSCache<NSString, UIImage>()
  33. private var petsData: [HomeViewController.PetSummary] = []
  34. override func viewDidLoad() {
  35. super.viewDidLoad()
  36. view.backgroundColor = UIColor(hex: "#FFFEFC")
  37. if let backImage = UIImage(named: "AddPet385") {
  38. let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel))
  39. navigationItem.leftBarButtonItem = backButton
  40. }
  41. buildUI()
  42. fetchPets()
  43. NotificationCenter.default.addObserver(self,
  44. selector: #selector(handlePetDidSave(_:)),
  45. name: .petDidSave,
  46. object: nil)
  47. }
  48. // 收到通知后刷新列表
  49. @objc private func handlePetDidSave(_ note: Notification) {
  50. DispatchQueue.main.async { [weak self] in
  51. self?.fetchPets()
  52. }
  53. }
  54. // 记得移除监听
  55. deinit {
  56. NotificationCenter.default.removeObserver(self, name: .petDidSave, object: nil)
  57. }
  58. @objc private func tapCancel() { navigationController?.popViewController(animated: true)
  59. }
  60. private func buildUI() {
  61. // 顶部标题
  62. view.addSubview(titleLabel)
  63. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  64. NSLayoutConstraint.activate([
  65. titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
  66. titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16)
  67. ])
  68. // 滚动容器
  69. view.addSubview(scrollView)
  70. scrollView.translatesAutoresizingMaskIntoConstraints = false
  71. scrollView.showsVerticalScrollIndicator = false
  72. NSLayoutConstraint.activate([
  73. scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
  74. scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  75. scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  76. scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  77. ])
  78. scrollView.addSubview(contentView)
  79. contentView.translatesAutoresizingMaskIntoConstraints = false
  80. NSLayoutConstraint.activate([
  81. contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
  82. contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
  83. contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
  84. contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
  85. contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
  86. ])
  87. // 垂直栈放卡片
  88. stack.axis = .vertical
  89. stack.spacing = 12
  90. stack.alignment = .fill
  91. contentView.addSubview(stack)
  92. stack.translatesAutoresizingMaskIntoConstraints = false
  93. NSLayoutConstraint.activate([
  94. stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
  95. stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
  96. stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
  97. stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24)
  98. ])
  99. }
  100. // MARK: - Networking
  101. private struct PetListResp: Codable {
  102. let code: FlexibleCode
  103. let msg: String?
  104. let data: [HomeViewController.PetSummary]
  105. }
  106. private func fetchPets() {
  107. let userId = UserDefaults.standard.string(forKey: "userId") ?? ""
  108. guard !userId.isEmpty else {
  109. #if DEBUG
  110. print("❌ 缺少 userId,无法请求宠物列表")
  111. #endif
  112. return
  113. }
  114. var comps = URLComponents(string: "\(baseURL)/petRecordPet/queryPetList")
  115. comps?.queryItems = [URLQueryItem(name: "userId", value: userId)]
  116. guard let url = comps?.url else { return }
  117. var req = URLRequest(url: url)
  118. req.httpMethod = "GET"
  119. req.setValue("application/json", forHTTPHeaderField: "Content-Type")
  120. if let token = UserDefaults.standard.string(forKey: "userToken") {
  121. req.setValue(token, forHTTPHeaderField: "Authorization")
  122. }
  123. print("🐶 GET: \(url.absoluteString)")
  124. URLSession.shared.dataTask(with: req) { [weak self] data, resp, err in
  125. if let http = resp as? HTTPURLResponse { print("🐶 status: \(http.statusCode)") }
  126. if let err = err { print("🐶 error: \(err)"); return }
  127. guard let data = data else { print("🐶 empty data"); return }
  128. do {
  129. let decoder = JSONDecoder()
  130. let r = try decoder.decode(PetListResp.self, from: data)
  131. if r.code.raw == "200" {
  132. DispatchQueue.main.async { self?.render(pets: r.data) }
  133. } else {
  134. print("🐶 server msg: \(r.msg ?? "-") code=\(r.code.raw)")
  135. }
  136. } catch {
  137. print("🐶 decode error: \(error)")
  138. }
  139. }.resume()
  140. }
  141. // MARK: - UI render
  142. private func render(pets: [HomeViewController.PetSummary]) {
  143. self.petsData = pets
  144. // 清空旧卡片
  145. stack.arrangedSubviews.forEach { v in
  146. stack.removeArrangedSubview(v)
  147. v.removeFromSuperview()
  148. }
  149. guard !pets.isEmpty else { return }
  150. let placeholder = UIImage(named: "Home372") ?? UIImage(systemName: "pawprint")!
  151. for (idx, p) in pets.enumerated() {
  152. let card = PetHeaderCard()
  153. card.translatesAutoresizingMaskIntoConstraints = false
  154. card.heightAnchor.constraint(equalToConstant: 103).isActive = true
  155. // 点击跳转
  156. card.isUserInteractionEnabled = true
  157. card.tag = idx
  158. let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped(_:)))
  159. card.addGestureRecognizer(tap)
  160. // 文案
  161. let breed = p.breedName ?? "宠物品种"
  162. let ageText = (p.age?.isEmpty == false) ? "\(p.age!)岁" : "年龄"
  163. let daysText = (p.togetherDays?.isEmpty == false) ? "一起生活的第\(p.togetherDays!)天" : "一起生活的第XX天"
  164. // 先占位
  165. card.configure(avatar: placeholder, name: p.name, breed: breed, ageText: ageText, daysText: daysText)
  166. // 异步加载头像
  167. if let urlStr = p.avatar, let url = URL(string: urlStr) {
  168. loadImage(from: url) { img in
  169. card.configure(avatar: img ?? placeholder, name: p.name, breed: breed, ageText: ageText, daysText: daysText)
  170. }
  171. }
  172. stack.addArrangedSubview(card)
  173. }
  174. }
  175. @objc private func cardTapped(_ g: UITapGestureRecognizer) {
  176. guard let v = g.view else { return }
  177. let index = v.tag
  178. guard index >= 0, index < petsData.count else { return }
  179. let pet = petsData[index]
  180. let vc = PetDetailViewController(pet: pet)
  181. navigationController?.pushViewController(vc, animated: true)
  182. }
  183. // MARK: - Image loader (no 3rd-party)
  184. private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
  185. let key = url.absoluteString as NSString
  186. if let c = imageCache.object(forKey: key) {
  187. completion(c)
  188. return
  189. }
  190. URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
  191. var img: UIImage? = nil
  192. if let data = data { img = UIImage(data: data) }
  193. if let i = img { self?.imageCache.setObject(i, forKey: key) }
  194. DispatchQueue.main.async { completion(img) }
  195. }.resume()
  196. }
  197. }