| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- //
- // 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<NSString, UIImage>()
- 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()
- }
- }
|