AddPetViewController.swift 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. //
  2. // AddPetViewController.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. // MARK: - AddPetViewController (添加宠物)
  10. final class AddPetViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  11. private let scrollView = UIScrollView()
  12. private let contentView = UIView()
  13. private let avatarButton = UIButton(type: .custom)
  14. private let avatarHint = UILabel()
  15. private let nickCard = UIView()
  16. private let nickTitle = UILabel()
  17. private let nickField = UITextField()
  18. private let infoCard = UIView()
  19. // rows
  20. private let weightRow = InputRow(title: "体重", placeholder: "请输入体重(kg)", showsChevron: false, keyboard: .decimalPad)
  21. private let speciesRow = SelectRow(title: "种类")
  22. private let breedRow = SelectRow(title: "品种")
  23. private let genderRow = SelectRow(title: "性别")
  24. private let birthRow = SelectRow(title: "出生日期")
  25. private let arriveRow = SelectRow(title: "到家日期")
  26. private let cancelBtn = UIButton(type: .system)
  27. private let confirmBtn = UIButton(type: .system)
  28. private var selectedSpecies: String?
  29. private var selectedBreed: String? // For display (breed name)
  30. private var selectedBreedId: String? // For storing breedId
  31. private var selectedGender: String?
  32. private var birthDate: Date?
  33. private var arriveDate: Date?
  34. // For avatar upload
  35. private var avatarImageUrl: String?
  36. override func viewDidLoad() {
  37. super.viewDidLoad()
  38. view.backgroundColor = .white
  39. title = "添加宠物"
  40. // Set custom back button image without text or back label
  41. if let backImage = UIImage(named: "AddPet385") {
  42. let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel))
  43. navigationItem.leftBarButtonItem = backButton
  44. }
  45. layout()
  46. wire()
  47. }
  48. private func layout() {
  49. // scroll
  50. view.addSubview(scrollView)
  51. scrollView.translatesAutoresizingMaskIntoConstraints = false
  52. scrollView.showsVerticalScrollIndicator = false
  53. scrollView.keyboardDismissMode = .onDrag
  54. scrollView.addSubview(contentView)
  55. contentView.translatesAutoresizingMaskIntoConstraints = false
  56. NSLayoutConstraint.activate([
  57. scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
  58. scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  59. scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  60. scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  61. contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
  62. contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
  63. contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
  64. contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
  65. contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
  66. ])
  67. // avatar
  68. avatarButton.translatesAutoresizingMaskIntoConstraints = false
  69. avatarButton.setImage(UIImage(named: "Home372") ?? UIImage(systemName: "pawprint.circle")!, for: .normal)
  70. avatarButton.imageView?.contentMode = .scaleAspectFill
  71. avatarButton.layer.cornerRadius = 40
  72. avatarButton.clipsToBounds = true
  73. avatarButton.backgroundColor = UIColor(hex: "#FFF3D9")
  74. avatarHint.text = "点击上传头像"
  75. avatarHint.font = .systemFont(ofSize: 12)
  76. avatarHint.textColor = UIColor(hex: "#8B8B8B")
  77. avatarHint.textAlignment = .center
  78. avatarHint.translatesAutoresizingMaskIntoConstraints = false
  79. contentView.addSubview(avatarButton)
  80. contentView.addSubview(avatarHint)
  81. NSLayoutConstraint.activate([
  82. avatarButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
  83. avatarButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
  84. avatarButton.widthAnchor.constraint(equalToConstant: 80),
  85. avatarButton.heightAnchor.constraint(equalToConstant: 80),
  86. avatarHint.topAnchor.constraint(equalTo: avatarButton.bottomAnchor, constant: 6),
  87. avatarHint.centerXAnchor.constraint(equalTo: avatarButton.centerXAnchor)
  88. ])
  89. // Nick card
  90. nickCard.backgroundColor = .white
  91. nickCard.layer.cornerRadius = 12
  92. nickCard.layer.shadowColor = UIColor.black.cgColor
  93. nickCard.layer.shadowOpacity = 0.06
  94. nickCard.layer.shadowRadius = 6
  95. nickCard.layer.shadowOffset = .init(width: 0, height: 2)
  96. nickCard.translatesAutoresizingMaskIntoConstraints = false
  97. nickTitle.text = "昵称"
  98. nickTitle.font = .systemFont(ofSize: 14)
  99. nickTitle.textColor = UIColor(hex: "#5B5B5B")
  100. nickField.placeholder = "请输入昵称"
  101. nickField.font = .systemFont(ofSize: 14)
  102. nickField.textAlignment = .right
  103. nickField.clearButtonMode = .whileEditing
  104. let nickSeparator = DashedSeparator()
  105. [nickTitle, nickField].forEach { nickCard.addSubview($0); $0.translatesAutoresizingMaskIntoConstraints = false }
  106. contentView.addSubview(nickCard)
  107. NSLayoutConstraint.activate([
  108. nickCard.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 120),
  109. nickCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
  110. nickCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
  111. nickCard.heightAnchor.constraint(equalToConstant: 64),
  112. nickTitle.centerYAnchor.constraint(equalTo: nickCard.centerYAnchor),
  113. nickTitle.leadingAnchor.constraint(equalTo: nickCard.leadingAnchor, constant: 16),
  114. nickField.centerYAnchor.constraint(equalTo: nickCard.centerYAnchor),
  115. nickField.trailingAnchor.constraint(equalTo: nickCard.trailingAnchor, constant: -24),
  116. nickField.leadingAnchor.constraint(greaterThanOrEqualTo: nickTitle.trailingAnchor, constant: 12)
  117. ])
  118. // Info card
  119. infoCard.backgroundColor = .white
  120. infoCard.layer.cornerRadius = 12
  121. infoCard.layer.shadowColor = UIColor.black.cgColor
  122. infoCard.layer.shadowOpacity = 0.06
  123. infoCard.layer.shadowRadius = 6
  124. infoCard.layer.shadowOffset = .init(width: 0, height: 2)
  125. infoCard.translatesAutoresizingMaskIntoConstraints = false
  126. contentView.addSubview(infoCard)
  127. let rows: [UIView] = [weightRow, speciesRow, breedRow, genderRow, birthRow, arriveRow]
  128. rows.forEach { r in
  129. r.translatesAutoresizingMaskIntoConstraints = false
  130. infoCard.addSubview(r)
  131. }
  132. // separators
  133. let seps = (0..<(rows.count-1)).map { _ in DashedSeparator() }
  134. seps.forEach { s in infoCard.addSubview(s); s.translatesAutoresizingMaskIntoConstraints = false }
  135. NSLayoutConstraint.activate([
  136. infoCard.topAnchor.constraint(equalTo: nickCard.bottomAnchor, constant: 16),
  137. infoCard.leadingAnchor.constraint(equalTo: nickCard.leadingAnchor),
  138. infoCard.trailingAnchor.constraint(equalTo: nickCard.trailingAnchor)
  139. ])
  140. // Row heights
  141. let h: CGFloat = 52
  142. var prev: UIView? = nil
  143. for (idx, row) in rows.enumerated() {
  144. NSLayoutConstraint.activate([
  145. row.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 12),
  146. row.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -12),
  147. row.heightAnchor.constraint(equalToConstant: h)
  148. ])
  149. if let p = prev {
  150. row.topAnchor.constraint(equalTo: p.bottomAnchor).isActive = true
  151. } else {
  152. row.topAnchor.constraint(equalTo: infoCard.topAnchor).isActive = true
  153. }
  154. if idx < seps.count {
  155. let sep = seps[idx]
  156. NSLayoutConstraint.activate([
  157. sep.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor, constant: 12),
  158. sep.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor, constant: -12),
  159. sep.topAnchor.constraint(equalTo: row.bottomAnchor),
  160. sep.heightAnchor.constraint(equalToConstant: 1)
  161. ])
  162. }
  163. prev = row
  164. }
  165. prev!.bottomAnchor.constraint(equalTo: infoCard.bottomAnchor).isActive = true
  166. // Bottom buttons
  167. cancelBtn.setTitle("取消", for: .normal)
  168. cancelBtn.setTitleColor(.white, for: .normal)
  169. cancelBtn.backgroundColor = UIColor(hex: "#CFC7BD")
  170. cancelBtn.layer.cornerRadius = 22
  171. confirmBtn.setTitle("确定", for: .normal)
  172. confirmBtn.setTitleColor(.black, for: .normal)
  173. confirmBtn.backgroundColor = UIColor(hex: "#FFE059")
  174. confirmBtn.layer.cornerRadius = 22
  175. [cancelBtn, confirmBtn].forEach { v in
  176. contentView.addSubview(v)
  177. v.translatesAutoresizingMaskIntoConstraints = false
  178. v.heightAnchor.constraint(equalToConstant: 44).isActive = true
  179. }
  180. NSLayoutConstraint.activate([
  181. cancelBtn.topAnchor.constraint(equalTo: infoCard.bottomAnchor, constant: 28),
  182. cancelBtn.leadingAnchor.constraint(equalTo: infoCard.leadingAnchor),
  183. cancelBtn.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant: -8),
  184. confirmBtn.topAnchor.constraint(equalTo: cancelBtn.topAnchor),
  185. confirmBtn.leadingAnchor.constraint(equalTo: view.centerXAnchor, constant: 8),
  186. confirmBtn.trailingAnchor.constraint(equalTo: infoCard.trailingAnchor),
  187. confirmBtn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -28)
  188. ])
  189. }
  190. private func wire() {
  191. let tap = UITapGestureRecognizer(target: self, action: #selector(endEdit))
  192. tap.cancelsTouchesInView = false
  193. view.addGestureRecognizer(tap)
  194. avatarButton.addTarget(self, action: #selector(pickAvatar), for: .touchUpInside)
  195. cancelBtn.addTarget(self, action: #selector(tapCancel), for: .touchUpInside)
  196. confirmBtn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside)
  197. speciesRow.onTap = { [weak self] in self?.chooseSpecies() }
  198. breedRow.onTap = { [weak self] in self?.chooseBreed() }
  199. genderRow.onTap = { [weak self] in self?.chooseGender() }
  200. birthRow.onTap = { [weak self] in self?.chooseDate(for: .birth) }
  201. arriveRow.onTap = { [weak self] in self?.chooseDate(for: .arrive) }
  202. }
  203. @objc private func endEdit() { view.endEditing(true) }
  204. // MARK: - Actions
  205. // Upload avatar image
  206. @objc private func pickAvatar() {
  207. let picker = UIImagePickerController()
  208. picker.sourceType = .photoLibrary
  209. picker.delegate = self
  210. present(picker, animated: true)
  211. }
  212. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
  213. if let img = info[.originalImage] as? UIImage {
  214. avatarButton.setImage(img, for: .normal)
  215. uploadAvatar(image: img) // Upload avatar after picking
  216. }
  217. picker.dismiss(animated: true)
  218. }
  219. private func uploadAvatar(image: UIImage) {
  220. guard let imageData = image.jpegData(compressionQuality: 0.7) else { return }
  221. let url = URL(string: "\(baseURL)/common/upload")!
  222. var request = URLRequest(url: url)
  223. request.httpMethod = "POST"
  224. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  225. if let token = UserDefaults.standard.string(forKey: "userToken") {
  226. request.setValue(token, forHTTPHeaderField: "Authorization")
  227. }
  228. // 创建多部分表单数据
  229. let boundary = "Boundary-\(UUID().uuidString)"
  230. request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
  231. var body = Data()
  232. // 添加图片数据
  233. body.append("--\(boundary)\r\n".data(using: .utf8)!)
  234. body.append("Content-Disposition: form-data; name=\"file\"; filename=\"avatar.jpg\"\r\n".data(using: .utf8)!)
  235. body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
  236. body.append(imageData)
  237. body.append("\r\n".data(using: .utf8)!)
  238. // 结束边界
  239. body.append("--\(boundary)--\r\n".data(using: .utf8)!)
  240. request.httpBody = body
  241. let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
  242. if let error = error {
  243. print("Error uploading image: \(error)")
  244. DispatchQueue.main.async {
  245. self?.showToast(message: "头像上传失败,请重试。")
  246. }
  247. return
  248. }
  249. guard let data = data else {
  250. DispatchQueue.main.async {
  251. self?.showToast(message: "头像上传失败,请重试。")
  252. }
  253. return
  254. }
  255. do {
  256. let decoder = JSONDecoder()
  257. let responseObject = try decoder.decode(UploadImageResponse.self, from: data)
  258. if responseObject.code == "200" {
  259. // Get imageUrl and save it
  260. self?.avatarImageUrl = responseObject.data.url
  261. print("ssss \(String(describing: self?.avatarImageUrl))")
  262. DispatchQueue.main.async {
  263. self?.showToast(message: "头像上传成功!")
  264. }
  265. } else {
  266. print("Error: \(responseObject.msg ?? "Unknown error")")
  267. DispatchQueue.main.async {
  268. self?.showToast(message: "头像上传失败,请重试。")
  269. }
  270. }
  271. } catch {
  272. print("Error decoding upload response: \(error)")
  273. DispatchQueue.main.async {
  274. self?.showToast(message: "头像上传失败,请重试。")
  275. }
  276. }
  277. }
  278. task.resume()
  279. }
  280. @objc private func tapCancel() { navigationController?.popViewController(animated: true) }
  281. @objc private func tapConfirm() {
  282. // Fetch breedId from selectedBreedId and categoryId from selectedSpecies
  283. let breedId = Int(selectedBreedId ?? "") ?? 0
  284. let categoryId = getCategoryId(from: selectedSpecies)
  285. let userId = UserDefaults.standard.integer(forKey: "userId") // Fetch userId from UserDefaults (or other storage)
  286. // DateFormatter to format the dates in the desired format
  287. let dateFormatter = DateFormatter()
  288. dateFormatter.dateFormat = "yyyy-MM-dd"
  289. // Format the arrivalDate and birthDate
  290. let formattedArrivalDate = dateFormatter.string(from: arriveDate ?? Date())
  291. let formattedBirthDate = dateFormatter.string(from: birthDate ?? Date())
  292. // Collect other data for pet save
  293. let name = nickField.text ?? ""
  294. let weight = weightRow.text
  295. let petData = PetSaveRequest(
  296. arrivalDate: formattedArrivalDate,
  297. avatar: avatarImageUrl ?? "",
  298. birthDate: formattedBirthDate,
  299. breedId: breedId,
  300. categoryId: categoryId,
  301. createTime: "",
  302. gender: selectedGender ?? "",
  303. id: 0,
  304. name: name,
  305. nickname: nickField.text ?? "",
  306. updateTime: "",
  307. userId: userId,
  308. weight: Double(weight ?? "0") ?? 0
  309. )
  310. print("dkkd \(petData)")
  311. savePetData(petData)
  312. }
  313. // Helper methods to get categoryId
  314. private func getCategoryId(from species: String?) -> Int {
  315. // Replace with actual logic to fetch categoryId based on species
  316. switch species {
  317. case "狗": return 3
  318. case "猫": return 4
  319. default: return 0
  320. }
  321. }
  322. private func savePetData(_ petData: PetSaveRequest) {
  323. guard let url = URL(string: "\(baseURL)/petRecordPet/petSave") else { return }
  324. var request = URLRequest(url: url)
  325. request.httpMethod = "POST"
  326. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  327. if let token = UserDefaults.standard.string(forKey: "userToken") {
  328. request.setValue(token, forHTTPHeaderField: "Authorization")
  329. }
  330. do {
  331. let encoder = JSONEncoder()
  332. let body = try encoder.encode(petData)
  333. request.httpBody = body
  334. } catch {
  335. print("Error encoding pet data: \(error)")
  336. DispatchQueue.main.async {
  337. self.showToast(message: "宠物信息保存失败,请重试。")
  338. }
  339. return
  340. }
  341. let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
  342. if let error = error {
  343. print("Error saving pet: \(error)")
  344. DispatchQueue.main.async {
  345. self?.showToast(message: "宠物信息保存失败,请重试。")
  346. }
  347. return
  348. }
  349. guard let data = data else {
  350. DispatchQueue.main.async {
  351. self?.showToast(message: "宠物信息保存失败,请重试。")
  352. }
  353. return
  354. }
  355. do {
  356. if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
  357. if let code = json["code"] as? String, code != "500" {
  358. print("Pet saved successfully!")
  359. // 通知首页刷新宠物列表
  360. let uidStr = UserDefaults.standard.string(forKey: "userId") ?? {
  361. let v = UserDefaults.standard.integer(forKey: "userId"); return v == 0 ? "" : String(v)
  362. }()
  363. NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": uidStr])
  364. DispatchQueue.main.async {
  365. self?.showToast(message: "宠物信息保存成功!")
  366. }
  367. // Add delay of 1.5 seconds before going back
  368. DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
  369. self?.navigationController?.popViewController(animated: true)
  370. }
  371. } else {
  372. DispatchQueue.main.async {
  373. self?.showToast(message: "宠物信息保存失败,请重试。")
  374. }
  375. }
  376. }
  377. } catch {
  378. print("Error decoding pet save response: \(error)")
  379. // 通知首页刷新宠物列表(异常也发通知)
  380. let uidStr = UserDefaults.standard.string(forKey: "userId") ?? {
  381. let v = UserDefaults.standard.integer(forKey: "userId"); return v == 0 ? "" : String(v)
  382. }()
  383. NotificationCenter.default.post(name: .petDidSave, object: nil, userInfo: ["userId": uidStr])
  384. DispatchQueue.main.async {
  385. self?.showToast(message: "宠物信息保存成功!")
  386. }
  387. DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
  388. self?.navigationController?.popViewController(animated: true)
  389. }
  390. }
  391. }
  392. task.resume()
  393. }
  394. // MARK: - Present action sheet safely on iPad
  395. private func presentSheet(_ ac: UIAlertController, from source: UIView) {
  396. if let pop = ac.popoverPresentationController {
  397. // 优先用触发的控件作为锚点
  398. if source.window != nil {
  399. pop.sourceView = source
  400. pop.sourceRect = source.bounds
  401. pop.permittedArrowDirections = [.up, .down]
  402. } else {
  403. // 兜底:用整个视图中心作为锚点(极少数情况下 source 还未在窗口层级里)
  404. pop.sourceView = self.view
  405. pop.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 1, height: 1)
  406. pop.permittedArrowDirections = []
  407. }
  408. }
  409. self.present(ac, animated: true)
  410. }
  411. // MARK: - Toast
  412. private func showToast(message: String) {
  413. 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))
  414. toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7)
  415. toastLabel.textColor = UIColor.white
  416. toastLabel.textAlignment = .center
  417. toastLabel.font = .systemFont(ofSize: 14)
  418. toastLabel.text = message
  419. toastLabel.alpha = 1.0
  420. toastLabel.layer.cornerRadius = 10
  421. toastLabel.clipsToBounds = true
  422. self.view.addSubview(toastLabel)
  423. UIView.animate(withDuration: 1, delay: 1, options: .curveEaseOut, animations: {
  424. toastLabel.alpha = 0.0
  425. }, completion: { _ in
  426. toastLabel.removeFromSuperview()
  427. })
  428. }
  429. // Models for requests and responses
  430. struct UploadImageResponse: Codable {
  431. let code: String
  432. let msg: String?
  433. let data: UploadImageData
  434. }
  435. struct UploadImageData: Codable {
  436. let url: String
  437. }
  438. struct PetSaveRequest: Codable {
  439. let arrivalDate: String
  440. let avatar: String
  441. let birthDate: String
  442. let breedId: Int
  443. let categoryId: Int
  444. let createTime: String
  445. let gender: String
  446. let id: Int
  447. let name: String
  448. let nickname: String
  449. let updateTime: String
  450. let userId: Int
  451. let weight: Double
  452. }
  453. struct PetSaveResponse: Codable {
  454. let code: String
  455. let msg: String?
  456. let data: PetSaveData?
  457. }
  458. struct PetSaveData: Codable {
  459. let petId: Int
  460. }
  461. private func chooseSpecies() {
  462. let ac = UIAlertController(title: "选择种类", message: nil, preferredStyle: .actionSheet)
  463. ["猫", "狗"].forEach { t in ac.addAction(UIAlertAction(title: t, style: .default, handler: { _ in
  464. self.selectedSpecies = t
  465. self.speciesRow.value = t
  466. // 清空品种
  467. self.selectedBreed = nil
  468. self.selectedBreedId = nil
  469. self.breedRow.value = "请选择"
  470. })) }
  471. ac.addAction(UIAlertAction(title: "取消", style: .cancel))
  472. // ✅ 关键:指定 popover 锚点
  473. presentSheet(ac, from: self.speciesRow)
  474. }
  475. private func chooseBreed() {
  476. guard let sp = selectedSpecies else {
  477. let ac = UIAlertController(title: "请先选择种类", message: nil, preferredStyle: .alert)
  478. ac.addAction(UIAlertAction(title: "确定", style: .default))
  479. present(ac, animated: true); return
  480. }
  481. let categoryId: Int
  482. switch sp {
  483. case "狗": categoryId = 3
  484. case "猫": categoryId = 4
  485. default: categoryId = 0
  486. }
  487. let vc = BreedPickerViewController(categoryId: categoryId, token: UserDefaults.standard.string(forKey: "userToken"))
  488. vc.onSelect = { [weak self] breedId, breedName in
  489. self?.selectedBreed = breedName
  490. self?.breedRow.value = breedName
  491. self?.selectedBreedId = breedId
  492. self?.navigationController?.popViewController(animated: true)
  493. }
  494. navigationController?.pushViewController(vc, animated: true)
  495. }
  496. private func chooseGender() {
  497. let ac = UIAlertController(title: "选择性别", message: nil, preferredStyle: .actionSheet)
  498. ["公", "母"].forEach { t in ac.addAction(UIAlertAction(title: t, style: .default, handler: { _ in
  499. self.selectedGender = t
  500. self.genderRow.value = t
  501. })) }
  502. ac.addAction(UIAlertAction(title: "取消", style: .cancel))
  503. // ✅ 关键:指定 popover 锚点
  504. presentSheet(ac, from: self.genderRow)
  505. }
  506. private enum DateKind { case birth, arrive }
  507. private func chooseDate(for kind: DateKind) {
  508. let initDate = (kind == .birth) ? (self.birthDate ?? Date()) : (self.arriveDate ?? Date())
  509. let vc = DatePickerSheetController(initial: initDate)
  510. vc.onDone = { [weak self] date in
  511. guard let self = self else { return }
  512. let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
  513. let text = fmt.string(from: date)
  514. if kind == .birth { self.birthDate = date; self.birthRow.value = text }
  515. else { self.arriveDate = date; self.arriveRow.value = text }
  516. }
  517. present(vc, animated: false)
  518. }
  519. }
  520. // MARK: - Helper Views
  521. final class DashedSeparator: UIView {
  522. private let shape = CAShapeLayer()
  523. override init(frame: CGRect) {
  524. super.init(frame: frame)
  525. backgroundColor = .clear
  526. shape.strokeColor = UIColor(hex: "#DADADA").cgColor
  527. shape.lineDashPattern = [4, 4]
  528. shape.lineWidth = 1
  529. layer.addSublayer(shape)
  530. }
  531. required init?(coder: NSCoder) { fatalError() }
  532. override func layoutSubviews() {
  533. super.layoutSubviews()
  534. shape.frame = bounds
  535. let path = UIBezierPath()
  536. path.move(to: CGPoint(x: 0, y: bounds.midY))
  537. path.addLine(to: CGPoint(x: bounds.width, y: bounds.midY))
  538. shape.path = path.cgPath
  539. }
  540. }
  541. final class InputRow: UIView {
  542. private let titleLabel = UILabel()
  543. private let textField = UITextField()
  544. init(title: String, placeholder: String, showsChevron: Bool, keyboard: UIKeyboardType) {
  545. super.init(frame: .zero)
  546. titleLabel.text = title
  547. titleLabel.font = .systemFont(ofSize: 14)
  548. titleLabel.textColor = UIColor(hex: "#5B5B5B")
  549. textField.placeholder = placeholder
  550. textField.font = .systemFont(ofSize: 14)
  551. textField.textAlignment = .right
  552. textField.keyboardType = keyboard
  553. addSubview(titleLabel)
  554. addSubview(textField)
  555. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  556. textField.translatesAutoresizingMaskIntoConstraints = false
  557. NSLayoutConstraint.activate([
  558. titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
  559. titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
  560. textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
  561. textField.centerYAnchor.constraint(equalTo: centerYAnchor),
  562. textField.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8)
  563. ])
  564. }
  565. func setEnabled(_ enabled: Bool) {
  566. isUserInteractionEnabled = enabled
  567. textField.textColor = enabled ? UIColor(hex: "#8B8B8B") : UIColor(hex: "#C8C8C8")
  568. titleLabel.textColor = enabled ? UIColor(hex: "#5B5B5B") : UIColor(hex: "#A0A0A0")
  569. }
  570. required init?(coder: NSCoder) { fatalError() }
  571. var text: String {
  572. get { textField.text ?? "" }
  573. set { textField.text = newValue }
  574. }
  575. }
  576. final class SelectRow: UIControl {
  577. private let titleLabel = UILabel()
  578. private let valueLabel = UILabel()
  579. private let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
  580. var onTap: (() -> Void)?
  581. // 外部控制:显示/隐藏右侧箭头;启用/禁用交互与样式
  582. func setChevronHidden(_ hidden: Bool) {
  583. chevron.isHidden = hidden
  584. }
  585. func setEnabled(_ enabled: Bool) {
  586. isUserInteractionEnabled = enabled
  587. valueLabel.textColor = enabled ? UIColor(hex: "#8B8B8B") : UIColor(hex: "#C8C8C8")
  588. titleLabel.textColor = enabled ? UIColor(hex: "#5B5B5B") : UIColor(hex: "#A0A0A0")
  589. }
  590. init(title: String) {
  591. super.init(frame: .zero)
  592. addTarget(self, action: #selector(tap), for: .touchUpInside)
  593. titleLabel.text = title
  594. titleLabel.font = .systemFont(ofSize: 14)
  595. titleLabel.textColor = UIColor(hex: "#5B5B5B")
  596. valueLabel.text = "请选择"
  597. valueLabel.font = .systemFont(ofSize: 14)
  598. valueLabel.textColor = UIColor(hex: "#8B8B8B")
  599. valueLabel.textAlignment = .right
  600. chevron.tintColor = UIColor(hex: "#C1C1C1")
  601. chevron.setContentHuggingPriority(.required, for: .horizontal)
  602. [titleLabel, valueLabel, chevron].forEach { addSubview($0); $0.translatesAutoresizingMaskIntoConstraints = false }
  603. NSLayoutConstraint.activate([
  604. titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
  605. titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
  606. chevron.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -2),
  607. chevron.centerYAnchor.constraint(equalTo: centerYAnchor),
  608. chevron.widthAnchor.constraint(equalToConstant: 10),
  609. valueLabel.trailingAnchor.constraint(equalTo: chevron.leadingAnchor, constant: -8),
  610. valueLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
  611. valueLabel.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8)
  612. ])
  613. }
  614. required init?(coder: NSCoder) { fatalError() }
  615. @objc private func tap() { onTap?() }
  616. var value: String? { get { valueLabel.text } set { valueLabel.text = newValue } }
  617. }
  618. // MARK: - DatePickerSheetController (仿原型样式的日期弹窗)
  619. final class DatePickerSheetController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
  620. var onDone: ((Date) -> Void)?
  621. private let initial: Date
  622. private let bg = UIControl()
  623. private let sheet = UIView()
  624. private let header = UIView()
  625. private let titleLabel = UILabel()
  626. private let closeBtn = UIButton(type: .custom)
  627. private let doneBtn = UIButton(type: .custom)
  628. private let picker = UIPickerView()
  629. private let highlight = UIView()
  630. private var years: [Int] = []
  631. private let months = Array(1...12)
  632. private var days: [Int] = []
  633. private var selYear: Int = 0
  634. private var selMonth: Int = 0
  635. private var selDay: Int = 0
  636. init(initial: Date) {
  637. self.initial = initial
  638. super.init(nibName: nil, bundle: nil)
  639. modalPresentationStyle = .overFullScreen
  640. modalTransitionStyle = .crossDissolve
  641. }
  642. required init?(coder: NSCoder) { fatalError() }
  643. override func viewDidLoad() {
  644. super.viewDidLoad()
  645. view.backgroundColor = .clear
  646. buildData()
  647. buildUI()
  648. applyInitialSelection()
  649. }
  650. private func buildData() {
  651. let cal = Calendar.current
  652. let nowYear = cal.component(.year, from: Date())
  653. years = Array((nowYear - 50)...(nowYear + 5))
  654. }
  655. private func buildUI() {
  656. // Dim background
  657. bg.backgroundColor = UIColor.black.withAlphaComponent(0.45)
  658. bg.addTarget(self, action: #selector(close), for: .touchUpInside)
  659. view.addSubview(bg)
  660. bg.translatesAutoresizingMaskIntoConstraints = false
  661. // Sheet
  662. sheet.backgroundColor = .white
  663. sheet.layer.cornerRadius = 16
  664. sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
  665. view.addSubview(sheet)
  666. sheet.translatesAutoresizingMaskIntoConstraints = false
  667. // Header
  668. titleLabel.text = "选择时间"
  669. titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
  670. titleLabel.textColor = UIColor(hex: "#2B2B2B")
  671. titleLabel.textAlignment = .center
  672. closeBtn.setImage(UIImage(named: "AddPet385"), for: .normal)
  673. // closeBtn.tintColor = UIColor(hex: "#2B2B2B")
  674. closeBtn.addTarget(self, action: #selector(close), for: .touchUpInside)
  675. doneBtn.setImage(UIImage(named: "AddPet389"), for: .normal)
  676. // doneBtn.tintColor = UIColor(hex: "#2B2B2B")
  677. doneBtn.addTarget(self, action: #selector(doneTap), for: .touchUpInside)
  678. header.translatesAutoresizingMaskIntoConstraints = false
  679. header.addSubview(titleLabel)
  680. header.addSubview(closeBtn)
  681. header.addSubview(doneBtn)
  682. [titleLabel, closeBtn, doneBtn].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
  683. // Picker
  684. picker.dataSource = self
  685. picker.delegate = self
  686. picker.translatesAutoresizingMaskIntoConstraints = false
  687. picker.backgroundColor = .clear
  688. // Highlight row (FFE059, 25% alpha)
  689. highlight.backgroundColor = UIColor(hex: "#FFE059").withAlphaComponent(0.25)
  690. highlight.layer.cornerRadius = 22
  691. highlight.isUserInteractionEnabled = false
  692. highlight.translatesAutoresizingMaskIntoConstraints = false
  693. sheet.addSubview(header)
  694. sheet.addSubview(picker)
  695. sheet.addSubview(highlight)
  696. // Layout
  697. NSLayoutConstraint.activate([
  698. bg.topAnchor.constraint(equalTo: view.topAnchor),
  699. bg.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  700. bg.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  701. bg.bottomAnchor.constraint(equalTo: view.bottomAnchor),
  702. sheet.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  703. sheet.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  704. sheet.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
  705. header.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8),
  706. header.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 12),
  707. header.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -12),
  708. header.heightAnchor.constraint(equalToConstant: 36),
  709. closeBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  710. closeBtn.leadingAnchor.constraint(equalTo: header.leadingAnchor),
  711. closeBtn.widthAnchor.constraint(equalToConstant: 28),
  712. closeBtn.heightAnchor.constraint(equalToConstant: 28),
  713. doneBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  714. doneBtn.trailingAnchor.constraint(equalTo: header.trailingAnchor),
  715. doneBtn.widthAnchor.constraint(equalToConstant: 28),
  716. doneBtn.heightAnchor.constraint(equalToConstant: 28),
  717. titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
  718. titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  719. picker.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 4),
  720. picker.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  721. picker.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  722. picker.bottomAnchor.constraint(equalTo: sheet.bottomAnchor),
  723. highlight.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 16),
  724. highlight.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -16),
  725. highlight.centerYAnchor.constraint(equalTo: picker.centerYAnchor),
  726. highlight.heightAnchor.constraint(equalToConstant: 44)
  727. ])
  728. }
  729. override func viewDidAppear(_ animated: Bool) {
  730. super.viewDidAppear(animated)
  731. // Present animation
  732. sheet.transform = CGAffineTransform(translationX: 0, y: 400)
  733. header.alpha = 0
  734. picker.alpha = 0
  735. bg.alpha = 0
  736. UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseOut]) {
  737. self.sheet.transform = .identity
  738. self.header.alpha = 1
  739. self.picker.alpha = 1
  740. self.bg.alpha = 1
  741. }
  742. }
  743. // MARK: - Picker data
  744. func numberOfComponents(in pickerView: UIPickerView) -> Int { 3 }
  745. func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
  746. switch component {
  747. case 0: return years.count
  748. case 1: return months.count
  749. default: return days.count
  750. }
  751. }
  752. func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
  753. let w = pickerView.bounds.width
  754. return component == 0 ? w * 0.4 : w * 0.3
  755. }
  756. func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { 44 }
  757. func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
  758. let label = (view as? UILabel) ?? UILabel()
  759. label.font = .systemFont(ofSize: 18, weight: .medium)
  760. label.textAlignment = component == 0 ? .center : .center
  761. label.textColor = UIColor(hex: "#2B2B2B")
  762. switch component {
  763. case 0: label.text = "\(years[row])年"
  764. case 1: label.text = String(format: "%02d月", months[row])
  765. default: label.text = String(format: "%02d日", days[row])
  766. }
  767. // 变灰非选中项
  768. let isSelected = row == pickerView.selectedRow(inComponent: component)
  769. label.textColor = isSelected ? UIColor(hex: "#2B2B2B") : UIColor(hex: "#A8A8A8")
  770. return label
  771. }
  772. func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
  773. switch component {
  774. case 0: selYear = years[row]
  775. case 1: selMonth = months[row]
  776. case 2: selDay = days[row]
  777. default: break
  778. }
  779. if component != 2 {
  780. rebuildDays()
  781. pickerView.reloadComponent(2)
  782. }
  783. // 让文字颜色即时更新
  784. pickerView.reloadComponent(component)
  785. }
  786. private func applyInitialSelection() {
  787. let cal = Calendar.current
  788. selYear = cal.component(.year, from: initial)
  789. selMonth = cal.component(.month, from: initial)
  790. selDay = cal.component(.day, from: initial)
  791. rebuildDays()
  792. if let yi = years.firstIndex(of: selYear) {
  793. picker.selectRow(yi, inComponent: 0, animated: false)
  794. }
  795. picker.selectRow(selMonth - 1, inComponent: 1, animated: false)
  796. if let di = days.firstIndex(of: selDay) {
  797. picker.selectRow(di, inComponent: 2, animated: false)
  798. }
  799. picker.reloadAllComponents()
  800. }
  801. private func rebuildDays() {
  802. let cal = Calendar.current
  803. var comps = DateComponents()
  804. comps.year = selYear
  805. comps.month = selMonth
  806. comps.day = 1
  807. let date = cal.date(from: comps) ?? Date()
  808. let range = cal.range(of: .day, in: .month, for: date) ?? 1..<31
  809. days = Array(range)
  810. if !days.contains(selDay) { selDay = days.last ?? 1 }
  811. }
  812. @objc private func close() { dismissAnimated() }
  813. @objc private func doneTap() {
  814. var comps = DateComponents()
  815. comps.year = selYear; comps.month = selMonth; comps.day = selDay
  816. let cal = Calendar.current
  817. let date = cal.date(from: comps) ?? Date()
  818. onDone?(date)
  819. dismissAnimated()
  820. }
  821. private func dismissAnimated() {
  822. UIView.animate(withDuration: 0.22, animations: {
  823. self.sheet.transform = CGAffineTransform(translationX: 0, y: 400)
  824. self.bg.alpha = 0
  825. }, completion: { _ in
  826. self.dismiss(animated: false)
  827. })
  828. }
  829. }