// // PetDetailViewController.swift // VenusKitto // // Created by Neoa on 2025/8/27. // import Foundation import UIKit // MARK: - PetDetailViewController (宠物详情 + 编辑/保存) final class PetDetailViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { // 输入/选择行与 AddPet 基本一致 private let scrollView = UIScrollView() private let contentView = UIView() private let avatarButton = UIButton(type: .custom) private let avatarHint = UILabel() private let nickCard = UIView() private let nickTitle = UILabel() private let nickField = UITextField() private let infoCard = UIView() private let weightRow = InputRow(title: "体重", placeholder: "请输入体重(kg)", showsChevron: false, keyboard: .decimalPad) private let speciesRow = SelectRow(title: "种类") private let breedRow = SelectRow(title: "品种") private let genderRow = SelectRow(title: "性别") private let birthRow = SelectRow(title: "出生日期") private let arriveRow = SelectRow(title: "到家日期") private let saveButton = UIButton(type: .system) private var isEditingMode = false { didSet { applyEditingState() } } private var selectedSpecies: String? private var selectedBreedName: String? private var selectedBreedId: String? private var selectedGender: String? private var birthDate: Date? private var arriveDate: Date? private var avatarImageUrl: String? private let pet: HomeViewController.PetSummary private let imageCache = NSCache() private let rightEditButton = UIButton(type: .system) init(pet: HomeViewController.PetSummary) { self.pet = pet super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white title = "宠物详情" // 左上角:返回 + 编辑 if let backImage = UIImage(named: "AddPet385") { let back = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapBack)) navigationItem.leftBarButtonItem = back } // 右上角:编辑/保存(自定义按钮以支持背景色) rightEditButton.setTitle("编辑", for: .normal) rightEditButton.setTitleColor(.black, for: .normal) rightEditButton.backgroundColor = .clear rightEditButton.layer.cornerRadius = 16 rightEditButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 14, bottom: 6, right: 14) rightEditButton.addTarget(self, action: #selector(onRightButtonTap), for: .touchUpInside) navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rightEditButton) layout() wire() fillWithPet() isEditingMode = false // 默认不可编辑(箭头隐藏、不可点) } @objc private func tapBack() { navigationController?.popViewController(animated: true) } private func layout() { // scroll view.addSubview(scrollView) scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.showsVerticalScrollIndicator = false scrollView.keyboardDismissMode = .onDrag scrollView.addSubview(contentView) contentView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 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) ]) // avatar avatarButton.translatesAutoresizingMaskIntoConstraints = false avatarButton.setImage(UIImage(named: "Home372") ?? UIImage(systemName: "pawprint.circle")!, for: .normal) avatarButton.imageView?.contentMode = .scaleAspectFill avatarButton.layer.cornerRadius = 40 avatarButton.clipsToBounds = true avatarButton.backgroundColor = UIColor(hex: "#FFF3D9") avatarHint.text = "点击上传头像" avatarHint.font = .systemFont(ofSize: 12) avatarHint.textColor = UIColor(hex: "#8B8B8B") avatarHint.textAlignment = .center avatarHint.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(avatarButton) contentView.addSubview(avatarHint) NSLayoutConstraint.activate([ avatarButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), avatarButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), avatarButton.widthAnchor.constraint(equalToConstant: 80), avatarButton.heightAnchor.constraint(equalToConstant: 80), avatarHint.topAnchor.constraint(equalTo: avatarButton.bottomAnchor, constant: 6), avatarHint.centerXAnchor.constraint(equalTo: avatarButton.centerXAnchor) ]) // Nick card nickCard.backgroundColor = .white nickCard.layer.cornerRadius = 12 nickCard.layer.shadowColor = UIColor.black.cgColor nickCard.layer.shadowOpacity = 0.06 nickCard.layer.shadowRadius = 6 nickCard.layer.shadowOffset = .init(width: 0, height: 2) nickCard.translatesAutoresizingMaskIntoConstraints = false nickTitle.text = "昵称" nickTitle.font = .systemFont(ofSize: 14) nickTitle.textColor = UIColor(hex: "#5B5B5B") nickField.placeholder = "请输入昵称" nickField.font = .systemFont(ofSize: 14) nickField.textAlignment = .right nickField.clearButtonMode = .whileEditing [nickTitle, nickField].forEach { nickCard.addSubview($0); $0.translatesAutoresizingMaskIntoConstraints = false } contentView.addSubview(nickCard) NSLayoutConstraint.activate([ nickCard.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 120), nickCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), nickCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), nickCard.heightAnchor.constraint(equalToConstant: 64), nickTitle.centerYAnchor.constraint(equalTo: nickCard.centerYAnchor), nickTitle.leadingAnchor.constraint(equalTo: nickCard.leadingAnchor, constant: 16), nickField.centerYAnchor.constraint(equalTo: nickCard.centerYAnchor), nickField.trailingAnchor.constraint(equalTo: nickCard.trailingAnchor, constant: -24), nickField.leadingAnchor.constraint(greaterThanOrEqualTo: nickTitle.trailingAnchor, constant: 12) ]) // Info card infoCard.backgroundColor = .white infoCard.layer.cornerRadius = 12 infoCard.layer.shadowColor = UIColor.black.cgColor infoCard.layer.shadowOpacity = 0.06 infoCard.layer.shadowRadius = 6 infoCard.layer.shadowOffset = .init(width: 0, height: 2) infoCard.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(infoCard) let rows: [UIView] = [weightRow, speciesRow, breedRow, genderRow, birthRow, arriveRow] rows.forEach { r in r.translatesAutoresizingMaskIntoConstraints = false infoCard.addSubview(r) } // 分隔线 let seps = (0..<(rows.count - 1)).map { _ in DashedSeparator() } seps.forEach { s in infoCard.addSubview(s); s.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ infoCard.topAnchor.constraint(equalTo: nickCard.bottomAnchor, constant: 16), infoCard.leadingAnchor.constraint(equalTo: nickCard.leadingAnchor), infoCard.trailingAnchor.constraint(equalTo: nickCard.trailingAnchor) ]) // Row heights let h: CGFloat = 52 var prev: UIView? = nil for (idx, row) in rows.enumerated() { NSLayoutConstraint.activate([ row.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 12), row.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -12), row.heightAnchor.constraint(equalToConstant: h) ]) if let p = prev { row.topAnchor.constraint(equalTo: p.bottomAnchor).isActive = true } else { row.topAnchor.constraint(equalTo: infoCard.topAnchor).isActive = true } if idx < seps.count { let sep = seps[idx] NSLayoutConstraint.activate([ sep.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 12), sep.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -12), sep.topAnchor.constraint(equalTo: row.bottomAnchor), sep.heightAnchor.constraint(equalToConstant: 1) ]) } prev = row } prev!.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor).isActive = true // Save button saveButton.setTitle("保存", for: .normal) saveButton.setTitleColor(.black, for: .normal) saveButton.backgroundColor = UIColor(hex: "#FFE059") saveButton.layer.cornerRadius = 22 contentView.addSubview(saveButton) saveButton.translatesAutoresizingMaskIntoConstraints = false saveButton.heightAnchor.constraint(equalToConstant: 44).isActive = true NSLayoutConstraint.activate([ saveButton.topAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: 28), saveButton.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor), saveButton.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor), saveButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -28) ]) } private func wire() { let tap = UITapGestureRecognizer(target: self, action: #selector(endEdit)) tap.cancelsTouchesInView = false view.addGestureRecognizer(tap) avatarButton.addTarget(self, action: #selector(pickAvatar), for: .touchUpInside) saveButton.addTarget(self, action: #selector(tapSave), for: .touchUpInside) speciesRow.onTap = { [weak self] in self?.chooseSpecies() } breedRow.onTap = { [weak self] in self?.chooseBreed() } genderRow.onTap = { [weak self] in self?.chooseGender() } birthRow.onTap = { [weak self] in self?.chooseDate(for: .birth) } arriveRow.onTap = { [weak self] in self?.chooseDate(for: .arrive) } } @objc private func endEdit() { view.endEditing(true) } // 只读/编辑模式切换 private func applyEditingState() { // chevron 显隐 & 交互 speciesRow.setChevronHidden(!isEditingMode) breedRow.setChevronHidden(!isEditingMode) genderRow.setChevronHidden(!isEditingMode) birthRow.setChevronHidden(!isEditingMode) arriveRow.setChevronHidden(!isEditingMode) weightRow.setEnabled(isEditingMode) speciesRow.setEnabled(isEditingMode) breedRow.setEnabled(isEditingMode) genderRow.setEnabled(isEditingMode) birthRow.setEnabled(isEditingMode) arriveRow.setEnabled(isEditingMode) nickField.isUserInteractionEnabled = isEditingMode avatarButton.isUserInteractionEnabled = isEditingMode // InputRow 的 textField是私有,这里不直接访问,整体禁用/启用这一行 weightRow.isUserInteractionEnabled = isEditingMode saveButton.isHidden = !isEditingMode avatarHint.isHidden = !isEditingMode updateRightButton() } // 更新右上角按钮外观 private func updateRightButton() { if isEditingMode { rightEditButton.setTitle("保存", for: .normal) rightEditButton.backgroundColor = UIColor(hex: "#FFE059") } else { rightEditButton.setTitle("编辑", for: .normal) rightEditButton.backgroundColor = .clear } } // 右上角按钮点击:编辑 -> 进入编辑;保存 -> 调用保存 @objc private func onRightButtonTap() { if isEditingMode { tapSave() } else { isEditingMode = true } } private func fillWithPet() { nickField.text = pet.nickname?.isEmpty == false ? pet.nickname : pet.name // 种类/品种/性别 if let cid = Int(pet.categoryId ?? "0") { selectedSpecies = (cid == 3) ? "狗" : (cid == 4 ? "猫" : nil) speciesRow.value = selectedSpecies ?? "请选择" } selectedBreedName = pet.breedName breedRow.value = pet.breedName ?? "请选择" selectedBreedId = pet.breedId selectedGender = pet.gender genderRow.value = pet.gender ?? "请选择" // 体重 if let w = pet.weight, !w.isEmpty { weightRow.text = w } // 日期 let dateFmt = DateFormatter(); dateFmt.dateFormat = "yyyy-MM-dd" if let d = parseServerDate(pet.birthDate) { birthDate = d; birthRow.value = dateFmt.string(from: d) } if let d = parseServerDate(pet.arrivalDate) { arriveDate = d; arriveRow.value = dateFmt.string(from: d) } // 头像 if let urlStr = pet.avatar, let url = URL(string: urlStr) { loadImage(from: url) { [weak self] img in self?.avatarButton.setImage(img ?? (UIImage(named: "Home372") ?? UIImage(systemName: "pawprint.circle")!), for: .normal) } } } // MARK: - Actions @objc private func pickAvatar() { guard isEditingMode else { return } let picker = UIImagePickerController() picker.sourceType = .photoLibrary picker.delegate = self present(picker, animated: true) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let img = info[.originalImage] as? UIImage { avatarButton.setImage(img, for: .normal) uploadAvatar(image: img) } picker.dismiss(animated: true) } private func uploadAvatar(image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 0.7) else { return } let url = URL(string: "\(baseURL)/common/upload")! var request = URLRequest(url: url) request.httpMethod = "POST" if let token = UserDefaults.standard.string(forKey: "userToken") { request.setValue(token, forHTTPHeaderField: "Authorization") } let boundary = "Boundary-\(UUID().uuidString)" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() body.append("--\(boundary)\r\n".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"file\"; filename=\"avatar.jpg\"\r\n".data(using: .utf8)!) body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!) body.append(imageData) body.append("\r\n".data(using: .utf8)!) body.append("--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in guard let data = data else { return } if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let code = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }), code == "200", let dataObj = json["data"] as? [String: Any], let urlStr = dataObj["url"] as? String { self?.avatarImageUrl = urlStr } }.resume() } @objc private func tapSave() { // 组装参数 let userId = Int(UserDefaults.standard.string(forKey: "userId") ?? "") ?? UserDefaults.standard.integer(forKey: "userId") let breedId = Int(selectedBreedId ?? (pet.breedId ?? "0")) ?? 0 let categoryId = (selectedSpecies == "狗") ? 3 : (selectedSpecies == "猫" ? 4 : (Int(pet.categoryId ?? "0") ?? 0)) let gender = selectedGender ?? (pet.gender ?? "") let name = nickField.text ?? pet.name let nickname = nickField.text ?? pet.nickname ?? pet.name let dateFormatter = DateFormatter(); dateFormatter.dateFormat = "yyyy-MM-dd" let birth = birthDate != nil ? dateFormatter.string(from: birthDate!) : (dateFormatter.string(from: parseServerDate(pet.birthDate) ?? Date())) let arrive = arriveDate != nil ? dateFormatter.string(from: arriveDate!) : (dateFormatter.string(from: parseServerDate(pet.arrivalDate) ?? Date())) let weightDouble: Double = { if let d = Double(weightRow.text), !weightRow.text.isEmpty { return d } if let w = pet.weight, let d = Double(w) { return d } return 0 }() let bodyDict: [String: Any] = [ "arrivalDate": arrive, "avatar": avatarImageUrl ?? (pet.avatar ?? ""), "birthDate": birth, "breedId": breedId, "categoryId": categoryId, "createTime": "", "gender": gender, "id": Int(pet.id ?? "0") ?? 0, "name": name, "nickname": nickname, "updateTime": "", "userId": userId, "weight": weightDouble ] guard let url = URL(string: "\(baseURL)/petRecordPet/petUpdate") else { return } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token = UserDefaults.standard.string(forKey: "userToken") { req.setValue(token, forHTTPHeaderField: "Authorization") } req.httpBody = try? JSONSerialization.data(withJSONObject: bodyDict, options: []) URLSession.shared.dataTask(with: req) { [weak self] data, resp, err in if let http = resp as? HTTPURLResponse { print("🐾 petUpdate status: \(http.statusCode)") } if let err = err { print("🐾 petUpdate error: \(err)"); return } guard let data = data else { return } if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { print() let code = (json["code"] as? String) ?? ((json["code"] as? Int).map { String($0) }) DispatchQueue.main.async { if code == "200" { DispatchQueue.main.async { self?.showToast("更新成功") // 通知首页/列表刷新 NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": String(userId)]) self?.navigationController?.popViewController(animated: true) } } else { DispatchQueue.main.async { self?.showToast("更新失败") } } } }else{ DispatchQueue.main.async { self?.showToast("更新成功") // 通知首页/列表刷新 NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": String(userId)]) self?.navigationController?.popViewController(animated: true) } } }.resume() } // MARK: - 选择器(与 AddPet 一致) private enum DateKind { case birth, arrive } private func chooseDate(for kind: DateKind) { guard isEditingMode else { return } let initDate = (kind == .birth) ? (self.birthDate ?? Date()) : (self.arriveDate ?? Date()) let vc = DatePickerSheetController(initial: initDate) vc.onDone = { [weak self] date in guard let self = self else { return } let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd" let text = fmt.string(from: date) if kind == .birth { self.birthDate = date; self.birthRow.value = text } else { self.arriveDate = date; self.arriveRow.value = text } } present(vc, animated: false) } private func chooseSpecies() { guard isEditingMode else { return } let ac = UIAlertController(title: "选择种类", message: nil, preferredStyle: .actionSheet) ["猫", "狗"].forEach { t in ac.addAction(UIAlertAction(title: t, style: .default, handler: { _ in self.selectedSpecies = t self.speciesRow.value = t // 清空品种 self.selectedBreedName = nil self.selectedBreedId = nil self.breedRow.value = "请选择" })) } ac.addAction(UIAlertAction(title: "取消", style: .cancel)) // ✅ 关键:指定 popover 锚点 presentSheet(ac, from: self.speciesRow) } private func chooseBreed() { guard isEditingMode else { return } guard let sp = selectedSpecies ?? ((Int(pet.categoryId ?? "0") ?? 0) == 3 ? "狗" : ((Int(pet.categoryId ?? "0") ?? 0) == 4 ? "猫" : nil)) else { let ac = UIAlertController(title: "请先选择种类", message: nil, preferredStyle: .alert) ac.addAction(UIAlertAction(title: "确定", style: .default)) present(ac, animated: true); return } let categoryId: Int switch sp { case "狗": categoryId = 3 case "猫": categoryId = 4 default: categoryId = 0 } let vc = BreedPickerViewController(categoryId: categoryId, token: UserDefaults.standard.string(forKey: "userToken")) vc.onSelect = { [weak self] breedId, breedName in self?.selectedBreedName = breedName self?.breedRow.value = breedName self?.selectedBreedId = breedId self?.navigationController?.popViewController(animated: true) } navigationController?.pushViewController(vc, animated: true) } private func chooseGender() { guard isEditingMode else { return } let ac = UIAlertController(title: "选择性别", message: nil, preferredStyle: .actionSheet) ["公", "母"].forEach { t in ac.addAction(UIAlertAction(title: t, style: .default, handler: { _ in self.selectedGender = t self.genderRow.value = t })) } ac.addAction(UIAlertAction(title: "取消", style: .cancel)) // ✅ 关键:指定 popover 锚点 presentSheet(ac, from: self.genderRow) } // MARK: - Present action sheet safely on iPad private func presentSheet(_ ac: UIAlertController, from source: UIView) { if let pop = ac.popoverPresentationController { // 优先用触发的控件作为锚点 if source.window != nil { pop.sourceView = source pop.sourceRect = source.bounds pop.permittedArrowDirections = [.up, .down] } else { // 兜底:用整个视图中心作为锚点(极少数情况下 source 还未在窗口层级里) pop.sourceView = self.view pop.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 1, height: 1) pop.permittedArrowDirections = [] } } self.present(ac, animated: true) } // MARK: - Helpers private func parseServerDate(_ s: String?) -> Date? { guard let s = s, !s.isEmpty else { return nil } // 兼容 2025-08-20T00:00:00.000+08:00 let fmt = DateFormatter() fmt.locale = Locale(identifier: "en_US_POSIX") fmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" return fmt.date(from: s) } 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() } private func showToast(_ message: String) { let toastLabel = UILabel(frame: CGRect(x: self.view.frame.size.width / 2 - 150, y: self.view.frame.size.height/2 - 50, width: 300, height: 50)) toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7) toastLabel.textColor = UIColor.white toastLabel.textAlignment = .center toastLabel.font = .systemFont(ofSize: 14) toastLabel.text = message toastLabel.alpha = 1.0 toastLabel.layer.cornerRadius = 10 toastLabel.clipsToBounds = true self.view.addSubview(toastLabel) UIView.animate(withDuration: 1, delay: 1, options: .curveEaseOut, animations: { toastLabel.alpha = 0.0 }, completion: { _ in toastLabel.removeFromSuperview() }) } }