// // AddPetViewController.swift // VenusKitto // // Created by Neoa on 2025/8/22. // import Foundation import UIKit // MARK: - AddPetViewController (添加宠物) final class AddPetViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 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() // rows 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 cancelBtn = UIButton(type: .system) private let confirmBtn = UIButton(type: .system) private var selectedSpecies: String? private var selectedBreed: String? // For display (breed name) private var selectedBreedId: String? // For storing breedId private var selectedGender: String? private var birthDate: Date? private var arriveDate: Date? // For avatar upload private var avatarImageUrl: String? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white title = "添加宠物" // Set custom back button image without text or back label if let backImage = UIImage(named: "AddPet385") { let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel)) navigationItem.leftBarButtonItem = backButton } layout() wire() } 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 let nickSeparator = DashedSeparator() [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) } // separators 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 // Bottom buttons cancelBtn.setTitle("取消", for: .normal) cancelBtn.setTitleColor(.white, for: .normal) cancelBtn.backgroundColor = UIColor(hex: "#CFC7BD") cancelBtn.layer.cornerRadius = 22 confirmBtn.setTitle("确定", for: .normal) confirmBtn.setTitleColor(.black, for: .normal) confirmBtn.backgroundColor = UIColor(hex: "#FFE059") confirmBtn.layer.cornerRadius = 22 [cancelBtn, confirmBtn].forEach { v in contentView.addSubview(v) v.translatesAutoresizingMaskIntoConstraints = false v.heightAnchor.constraint(equalToConstant: 44).isActive = true } NSLayoutConstraint.activate([ cancelBtn.topAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: 28), cancelBtn.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor), cancelBtn.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant: -8), confirmBtn.topAnchor.constraint(equalTo: cancelBtn.topAnchor), confirmBtn.leadingAnchor.constraint(equalTo: view.centerXAnchor, constant: 8), confirmBtn.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor), confirmBtn.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) cancelBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside) confirmBtn.addTarget(self, action: #selector(tapConfirm), 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) } // MARK: - Actions // Upload avatar image @objc private func pickAvatar() { 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) // Upload avatar after picking } 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" request.setValue("application/json", forHTTPHeaderField: "Content-Type") 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 let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in if let error = error { print("Error uploading image: \(error)") DispatchQueue.main.async { self?.showToast(message: "头像上传失败,请重试。") } return } guard let data = data else { DispatchQueue.main.async { self?.showToast(message: "头像上传失败,请重试。") } return } do { let decoder = JSONDecoder() let responseObject = try decoder.decode(UploadImageResponse.self, from: data) if responseObject.code == "200" { // Get imageUrl and save it self?.avatarImageUrl = responseObject.data.url print("ssss \(String(describing: self?.avatarImageUrl))") DispatchQueue.main.async { self?.showToast(message: "头像上传成功!") } } else { print("Error: \(responseObject.msg ?? "Unknown error")") DispatchQueue.main.async { self?.showToast(message: "头像上传失败,请重试。") } } } catch { print("Error decoding upload response: \(error)") DispatchQueue.main.async { self?.showToast(message: "头像上传失败,请重试。") } } } task.resume() } @objc private func tapCancel() { navigationController?.popViewController(animated: true) } @objc private func tapConfirm() { // Fetch breedId from selectedBreedId and categoryId from selectedSpecies let breedId = Int(selectedBreedId ?? "") ?? 0 let categoryId = getCategoryId(from: selectedSpecies) let userId = UserDefaults.standard.integer(forKey: "userId") // Fetch userId from UserDefaults (or other storage) // DateFormatter to format the dates in the desired format let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" // Format the arrivalDate and birthDate let formattedArrivalDate = dateFormatter.string(from: arriveDate ?? Date()) let formattedBirthDate = dateFormatter.string(from: birthDate ?? Date()) // Collect other data for pet save let name = nickField.text ?? "" let weight = weightRow.text let petData = PetSaveRequest( arrivalDate: formattedArrivalDate, avatar: avatarImageUrl ?? "", birthDate: formattedBirthDate, breedId: breedId, categoryId: categoryId, createTime: "", gender: selectedGender ?? "", id: 0, name: name, nickname: nickField.text ?? "", updateTime: "", userId: userId, weight: Double(weight ?? "0") ?? 0 ) print("dkkd \(petData)") savePetData(petData) } // Helper methods to get categoryId private func getCategoryId(from species: String?) -> Int { // Replace with actual logic to fetch categoryId based on species switch species { case "狗": return 3 case "猫": return 4 default: return 0 } } private func savePetData(_ petData: PetSaveRequest) { guard let url = URL(string: "\(baseURL)/petRecordPet/petSave") else { return } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") if let token = UserDefaults.standard.string(forKey: "userToken") { request.setValue(token, forHTTPHeaderField: "Authorization") } do { let encoder = JSONEncoder() let body = try encoder.encode(petData) request.httpBody = body } catch { print("Error encoding pet data: \(error)") DispatchQueue.main.async { self.showToast(message: "宠物信息保存失败,请重试。") } return } let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in if let error = error { print("Error saving pet: \(error)") DispatchQueue.main.async { self?.showToast(message: "宠物信息保存失败,请重试。") } return } guard let data = data else { DispatchQueue.main.async { self?.showToast(message: "宠物信息保存失败,请重试。") } return } do { if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { if let code = json["code"] as? String, code != "500" { print("Pet saved successfully!") // 通知首页刷新宠物列表 let uidStr = UserDefaults.standard.string(forKey: "userId") ?? { let v = UserDefaults.standard.integer(forKey: "userId"); return v == 0 ? "" : String(v) }() NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": uidStr]) DispatchQueue.main.async { self?.showToast(message: "宠物信息保存成功!") } // Add delay of 1.5 seconds before going back DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.navigationController?.popViewController(animated: true) } } else { DispatchQueue.main.async { self?.showToast(message: "宠物信息保存失败,请重试。") } } } } catch { print("Error decoding pet save response: \(error)") // 通知首页刷新宠物列表(异常也发通知) let uidStr = UserDefaults.standard.string(forKey: "userId") ?? { let v = UserDefaults.standard.integer(forKey: "userId"); return v == 0 ? "" : String(v) }() NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": uidStr]) DispatchQueue.main.async { self?.showToast(message: "宠物信息保存成功!") } DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.navigationController?.popViewController(animated: true) } } } task.resume() } // 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: - Toast 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() }) } // Models for requests and responses struct UploadImageResponse: Codable { let code: String let msg: String? let data: UploadImageData } struct UploadImageData: Codable { let url: String } struct PetSaveRequest: Codable { let arrivalDate: String let avatar: String let birthDate: String let breedId: Int let categoryId: Int let createTime: String let gender: String let id: Int let name: String let nickname: String let updateTime: String let userId: Int let weight: Double } struct PetSaveResponse: Codable { let code: String let msg: String? let data: PetSaveData? } struct PetSaveData: Codable { let petId: Int } private func chooseSpecies() { 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.selectedBreed = nil self.selectedBreedId = nil self.breedRow.value = "请选择" })) } ac.addAction(UIAlertAction(title: "取消", style: .cancel)) // ✅ 关键:指定 popover 锚点 presentSheet(ac, from: self.speciesRow) } private func chooseBreed() { guard let sp = selectedSpecies 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?.selectedBreed = breedName self?.breedRow.value = breedName self?.selectedBreedId = breedId self?.navigationController?.popViewController(animated: true) } navigationController?.pushViewController(vc, animated: true) } private func chooseGender() { 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) } private enum DateKind { case birth, arrive } private func chooseDate(for kind: DateKind) { 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) } } // MARK: - Helper Views final class DashedSeparator: UIView { private let shape = CAShapeLayer() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear shape.strokeColor = UIColor(hex: "#DADADA").cgColor shape.lineDashPattern = [4, 4] shape.lineWidth = 1 layer.addSublayer(shape) } required init?(coder: NSCoder) { fatalError() } override func layoutSubviews() { super.layoutSubviews() shape.frame = bounds let path = UIBezierPath() path.move(to: CGPoint(x: 0, y: bounds.midY)) path.addLine(to: CGPoint(x: bounds.width, y: bounds.midY)) shape.path = path.cgPath } } final class InputRow: UIView { private let titleLabel = UILabel() private let textField = UITextField() init(title: String, placeholder: String, showsChevron: Bool, keyboard: UIKeyboardType) { super.init(frame: .zero) titleLabel.text = title titleLabel.font = .systemFont(ofSize: 14) titleLabel.textColor = UIColor(hex: "#5B5B5B") textField.placeholder = placeholder textField.font = .systemFont(ofSize: 14) textField.textAlignment = .right textField.keyboardType = keyboard addSubview(titleLabel) addSubview(textField) titleLabel.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4), titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), textField.centerYAnchor.constraint(equalTo: centerYAnchor), textField.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8) ]) } func setEnabled(_ enabled: Bool) { isUserInteractionEnabled = enabled textField.textColor = enabled ? UIColor(hex: "#8B8B8B") : UIColor(hex: "#C8C8C8") titleLabel.textColor = enabled ? UIColor(hex: "#5B5B5B") : UIColor(hex: "#A0A0A0") } required init?(coder: NSCoder) { fatalError() } var text: String { get { textField.text ?? "" } set { textField.text = newValue } } } final class SelectRow: UIControl { private let titleLabel = UILabel() private let valueLabel = UILabel() private let chevron = UIImageView(image: UIImage(systemName: "chevron.right")) var onTap: (() -> Void)? // 外部控制:显示/隐藏右侧箭头;启用/禁用交互与样式 func setChevronHidden(_ hidden: Bool) { chevron.isHidden = hidden } func setEnabled(_ enabled: Bool) { isUserInteractionEnabled = enabled valueLabel.textColor = enabled ? UIColor(hex: "#8B8B8B") : UIColor(hex: "#C8C8C8") titleLabel.textColor = enabled ? UIColor(hex: "#5B5B5B") : UIColor(hex: "#A0A0A0") } init(title: String) { super.init(frame: .zero) addTarget(self, action: #selector(tap), for: .touchUpInside) titleLabel.text = title titleLabel.font = .systemFont(ofSize: 14) titleLabel.textColor = UIColor(hex: "#5B5B5B") valueLabel.text = "请选择" valueLabel.font = .systemFont(ofSize: 14) valueLabel.textColor = UIColor(hex: "#8B8B8B") valueLabel.textAlignment = .right chevron.tintColor = UIColor(hex: "#C1C1C1") chevron.setContentHuggingPriority(.required, for: .horizontal) [titleLabel, valueLabel, chevron].forEach { addSubview($0); $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4), titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), chevron.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -2), chevron.centerYAnchor.constraint(equalTo: centerYAnchor), chevron.widthAnchor.constraint(equalToConstant: 10), valueLabel.trailingAnchor.constraint(equalTo: chevron.leadingAnchor, constant: -8), valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor), valueLabel.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8) ]) } required init?(coder: NSCoder) { fatalError() } @objc private func tap() { onTap?() } var value: String? { get { valueLabel.text } set { valueLabel.text = newValue } } } // MARK: - DatePickerSheetController (仿原型样式的日期弹窗) final class DatePickerSheetController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { var onDone: ((Date) -> Void)? private let initial: Date private let bg = UIControl() private let sheet = UIView() private let header = UIView() private let titleLabel = UILabel() private let closeBtn = UIButton(type: .custom) private let doneBtn = UIButton(type: .custom) private let picker = UIPickerView() private let highlight = UIView() private var years: [Int] = [] private let months = Array(1...12) private var days: [Int] = [] private var selYear: Int = 0 private var selMonth: Int = 0 private var selDay: Int = 0 init(initial: Date) { self.initial = initial super.init(nibName: nil, bundle: nil) modalPresentationStyle = .overFullScreen modalTransitionStyle = .crossDissolve } required init?(coder: NSCoder) { fatalError() } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear buildData() buildUI() applyInitialSelection() } private func buildData() { let cal = Calendar.current let nowYear = cal.component(.year, from: Date()) years = Array((nowYear - 50)...(nowYear + 5)) } private func buildUI() { // Dim background bg.backgroundColor = UIColor.black.withAlphaComponent(0.45) bg.addTarget(self, action: #selector(close), for: .touchUpInside) view.addSubview(bg) bg.translatesAutoresizingMaskIntoConstraints = false // Sheet sheet.backgroundColor = .white sheet.layer.cornerRadius = 16 sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] view.addSubview(sheet) sheet.translatesAutoresizingMaskIntoConstraints = false // Header titleLabel.text = "选择时间" titleLabel.font = .systemFont(ofSize: 16, weight: .medium) titleLabel.textColor = UIColor(hex: "#2B2B2B") titleLabel.textAlignment = .center closeBtn.setImage(UIImage(named: "AddPet385"), for: .normal) // closeBtn.tintColor = UIColor(hex: "#2B2B2B") closeBtn.addTarget(self, action: #selector(close), for: .touchUpInside) doneBtn.setImage(UIImage(named: "AddPet389"), for: .normal) // doneBtn.tintColor = UIColor(hex: "#2B2B2B") doneBtn.addTarget(self, action: #selector(doneTap), for: .touchUpInside) header.translatesAutoresizingMaskIntoConstraints = false header.addSubview(titleLabel) header.addSubview(closeBtn) header.addSubview(doneBtn) [titleLabel, closeBtn, doneBtn].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } // Picker picker.dataSource = self picker.delegate = self picker.translatesAutoresizingMaskIntoConstraints = false picker.backgroundColor = .clear // Highlight row (FFE059, 25% alpha) highlight.backgroundColor = UIColor(hex: "#FFE059").withAlphaComponent(0.25) highlight.layer.cornerRadius = 22 highlight.isUserInteractionEnabled = false highlight.translatesAutoresizingMaskIntoConstraints = false sheet.addSubview(header) sheet.addSubview(picker) sheet.addSubview(highlight) // Layout NSLayoutConstraint.activate([ bg.topAnchor.constraint(equalTo: view.topAnchor), bg.leadingAnchor.constraint(equalTo: view.leadingAnchor), bg.trailingAnchor.constraint(equalTo: view.trailingAnchor), bg.bottomAnchor.constraint(equalTo: view.bottomAnchor), sheet.leadingAnchor.constraint(equalTo: view.leadingAnchor), sheet.trailingAnchor.constraint(equalTo: view.trailingAnchor), sheet.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), header.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8), header.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 12), header.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -12), header.heightAnchor.constraint(equalToConstant: 36), closeBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor), closeBtn.leadingAnchor.constraint(equalTo: header.leadingAnchor), closeBtn.widthAnchor.constraint(equalToConstant: 28), closeBtn.heightAnchor.constraint(equalToConstant: 28), doneBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor), doneBtn.trailingAnchor.constraint(equalTo: header.trailingAnchor), doneBtn.widthAnchor.constraint(equalToConstant: 28), doneBtn.heightAnchor.constraint(equalToConstant: 28), titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor), titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor), picker.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 4), picker.leadingAnchor.constraint(equalTo: sheet.leadingAnchor), picker.trailingAnchor.constraint(equalTo: sheet.trailingAnchor), picker.bottomAnchor.constraint(equalTo: sheet.bottomAnchor), highlight.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 16), highlight.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -16), highlight.centerYAnchor.constraint(equalTo: picker.centerYAnchor), highlight.heightAnchor.constraint(equalToConstant: 44) ]) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Present animation sheet.transform = CGAffineTransform(translationX: 0, y: 400) header.alpha = 0 picker.alpha = 0 bg.alpha = 0 UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) { self.sheet.transform = .identity self.header.alpha = 1 self.picker.alpha = 1 self.bg.alpha = 1 } } // MARK: - Picker data func numberOfComponents(in pickerView: UIPickerView) -> Int { 3 } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { switch component { case 0: return years.count case 1: return months.count default: return days.count } } func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat { let w = pickerView.bounds.width return component == 0 ? w * 0.4 : w * 0.3 } func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { 44 } func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { let label = (view as? UILabel) ?? UILabel() label.font = .systemFont(ofSize: 18, weight: .medium) label.textAlignment = component == 0 ? .center : .center label.textColor = UIColor(hex: "#2B2B2B") switch component { case 0: label.text = "\(years[row])年" case 1: label.text = String(format: "%02d月", months[row]) default: label.text = String(format: "%02d日", days[row]) } // 变灰非选中项 let isSelected = row == pickerView.selectedRow(inComponent: component) label.textColor = isSelected ? UIColor(hex: "#2B2B2B") : UIColor(hex: "#A8A8A8") return label } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { switch component { case 0: selYear = years[row] case 1: selMonth = months[row] case 2: selDay = days[row] default: break } if component != 2 { rebuildDays() pickerView.reloadComponent(2) } // 让文字颜色即时更新 pickerView.reloadComponent(component) } private func applyInitialSelection() { let cal = Calendar.current selYear = cal.component(.year, from: initial) selMonth = cal.component(.month, from: initial) selDay = cal.component(.day, from: initial) rebuildDays() if let yi = years.firstIndex(of: selYear) { picker.selectRow(yi, inComponent: 0, animated: false) } picker.selectRow(selMonth - 1, inComponent: 1, animated: false) if let di = days.firstIndex(of: selDay) { picker.selectRow(di, inComponent: 2, animated: false) } picker.reloadAllComponents() } private func rebuildDays() { let cal = Calendar.current var comps = DateComponents() comps.year = selYear comps.month = selMonth comps.day = 1 let date = cal.date(from: comps) ?? Date() let range = cal.range(of: .day, in: .month, for: date) ?? 1..<31 days = Array(range) if !days.contains(selDay) { selDay = days.last ?? 1 } } @objc private func close() { dismissAnimated() } @objc private func doneTap() { var comps = DateComponents() comps.year = selYear; comps.month = selMonth; comps.day = selDay let cal = Calendar.current let date = cal.date(from: comps) ?? Date() onDone?(date) dismissAnimated() } private func dismissAnimated() { UIView.animate(withDuration: 0.22, animations: { self.sheet.transform = CGAffineTransform(translationX: 0, y: 400) self.bg.alpha = 0 }, completion: { _ in self.dismiss(animated: false) }) } }