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