|
|
@@ -0,0 +1,961 @@
|
|
|
+//
|
|
|
+// 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)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|