// // MyPetsViewController.swift // VenusKitto // // Created by Neoa on 2025/8/27. // import Foundation import UIKit // MARK: - FlexibleCode (string or int) fileprivate struct FlexibleCode: Codable { let raw: String init(from decoder: Decoder) throws { let c = try decoder.singleValueContainer() if let s = try? c.decode(String.self) { raw = s } else if let i = try? c.decode(Int.self) { raw = String(i) } else { raw = "" } } } // MARK: - 我的宠物 列表页 final class MyPetsViewController: UIViewController { // UI private let titleLabel: UILabel = { let l = UILabel() l.text = "我的宠物" l.font = .systemFont(ofSize: 28, weight: .semibold) l.textColor = UIColor(hex: "#2B2B2B") return l }() private let scrollView = UIScrollView() private let contentView = UIView() private let stack = UIStackView() private let imageCache = NSCache() private var petsData: [HomeViewController.PetSummary] = [] override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = UIColor(hex: "#FFFEFC") if let backImage = UIImage(named: "AddPet385") { let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel)) navigationItem.leftBarButtonItem = backButton } buildUI() fetchPets() NotificationCenter.default.addObserver(self, selector: #selector(handlePetDidSave(_:)), name: .petDidSave, object: nil) } // 收到通知后刷新列表 @objc private func handlePetDidSave(_ note: Notification) { DispatchQueue.main.async { [weak self] in self?.fetchPets() } } // 记得移除监听 deinit { NotificationCenter.default.removeObserver(self, name: .petDidSave, object: nil) } @objc private func tapCancel() { navigationController?.popViewController(animated: true) } private func buildUI() { // 顶部标题 view.addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12), titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16) ]) // 滚动容器 view.addSubview(scrollView) scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.showsVerticalScrollIndicator = false NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) scrollView.addSubview(contentView) contentView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) // 垂直栈放卡片 stack.axis = .vertical stack.spacing = 12 stack.alignment = .fill contentView.addSubview(stack) stack.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12), stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12), stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24) ]) } // MARK: - Networking private struct PetListResp: Codable { let code: FlexibleCode let msg: String? let data: [HomeViewController.PetSummary] } private func fetchPets() { let userId = UserDefaults.standard.string(forKey: "userId") ?? "" guard !userId.isEmpty else { #if DEBUG print("❌ 缺少 userId,无法请求宠物列表") #endif return } var comps = URLComponents(string: "\(baseURL)/petRecordPet/queryPetList") comps?.queryItems = [URLQueryItem(name: "userId", value: userId)] guard let url = comps?.url else { return } var req = URLRequest(url: url) req.httpMethod = "GET" req.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token = UserDefaults.standard.string(forKey: "userToken") { req.setValue(token, forHTTPHeaderField: "Authorization") } print("🐶 GET: \(url.absoluteString)") URLSession.shared.dataTask(with: req) { [weak self] data, resp, err in if let http = resp as? HTTPURLResponse { print("🐶 status: \(http.statusCode)") } if let err = err { print("🐶 error: \(err)"); return } guard let data = data else { print("🐶 empty data"); return } do { let decoder = JSONDecoder() let r = try decoder.decode(PetListResp.self, from: data) if r.code.raw == "200" { DispatchQueue.main.async { self?.render(pets: r.data) } } else { print("🐶 server msg: \(r.msg ?? "-") code=\(r.code.raw)") } } catch { print("🐶 decode error: \(error)") } }.resume() } // MARK: - UI render private func render(pets: [HomeViewController.PetSummary]) { self.petsData = pets // 清空旧卡片 stack.arrangedSubviews.forEach { v in stack.removeArrangedSubview(v) v.removeFromSuperview() } guard !pets.isEmpty else { return } let placeholder = UIImage(named: "Home372") ?? UIImage(systemName: "pawprint")! for (idx, p) in pets.enumerated() { let card = PetHeaderCard() card.translatesAutoresizingMaskIntoConstraints = false card.heightAnchor.constraint(equalToConstant: 103).isActive = true // 点击跳转 card.isUserInteractionEnabled = true card.tag = idx let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped(_:))) card.addGestureRecognizer(tap) // 文案 let breed = p.breedName ?? "宠物品种" let ageText = (p.age?.isEmpty == false) ? "\(p.age!)岁" : "年龄" let daysText = (p.togetherDays?.isEmpty == false) ? "一起生活的第\(p.togetherDays!)天" : "一起生活的第XX天" // 先占位 card.configure(avatar: placeholder, name: p.name, breed: breed, ageText: ageText, daysText: daysText) // 异步加载头像 if let urlStr = p.avatar, let url = URL(string: urlStr) { loadImage(from: url) { img in card.configure(avatar: img ?? placeholder, name: p.name, breed: breed, ageText: ageText, daysText: daysText) } } stack.addArrangedSubview(card) } } @objc private func cardTapped(_ g: UITapGestureRecognizer) { guard let v = g.view else { return } let index = v.tag guard index >= 0, index < petsData.count else { return } let pet = petsData[index] let vc = PetDetailViewController(pet: pet) navigationController?.pushViewController(vc, animated: true) } // MARK: - Image loader (no 3rd-party) private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { let key = url.absoluteString as NSString if let c = imageCache.object(forKey: key) { completion(c) return } URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in var img: UIImage? = nil if let data = data { img = UIImage(data: data) } if let i = img { self?.imageCache.setObject(i, forKey: key) } DispatchQueue.main.async { completion(img) } }.resume() } }