| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818 |
- //
- // HomeViewController.swift
- // VenusKitto
- //
- // Created by Neoa on 2025/8/22.
- //
- import Foundation
- import UIKit
- // MARK: - HomeViewController
- class HomeViewController: UIViewController, UIPopoverPresentationControllerDelegate {
- // UI
- private let scrollView = UIScrollView()
- private let contentView = UIView()
- private let titleLabel: UILabel = {
- let l = UILabel()
- l.text = "记录"
- l.font = .systemFont(ofSize: 28, weight: .semibold)
- l.textColor = UIColor(hex: "#2B2B2B")
- return l
- }()
- private lazy var settingsButton: UIButton = {
- let b = UIButton(type: .system)
- if let img = UIImage(systemName: "gearshape") {
- b.setImage(img, for: .normal)
- } else {
- b.setTitle("⚙️", for: .normal)
- }
- b.tintColor = UIColor(hex: "#2B2B2B")
- b.addTarget(self, action: #selector(tapSettings), for: .touchUpInside)
- return b
- }()
- private let petCard = PetHeaderCard()
- private let tagScroll = UIScrollView()
- private let tagStack = UIStackView()
- private let recordStack = UIStackView()
- private let addButton: UIButton = {
- let b = UIButton(type: .custom)
- b.setImage(UIImage(named: "Home357"), for: .normal)
- // b.tintColor = .black
- // b.backgroundColor = UIColor(hex: "#FFE059")
- b.layer.cornerRadius = 28
- b.layer.shadowColor = UIColor.black.cgColor
- b.layer.shadowOpacity = 0.15
- b.layer.shadowRadius = 8
- b.layer.shadowOffset = .init(width: 0, height: 4)
- b.addTarget(self, action: #selector(tapAdd), for: .touchUpInside)
- return b
- }()
- private let tabBarView = BottomTabBar()
- // --- Action menu (pop when tapping +) ---
- private var isActionMenuShown = false
- private let overlayView = UIControl()
- private let glowView = UIView()
- private let leftActionStack = UIStackView()
- private let leftActionButton = UIButton(type: .custom)
- private let leftActionLabel = UILabel()
- private let rightActionStack = UIStackView()
- private let rightActionButton = UIButton(type: .custom)
- private let rightActionLabel = UILabel()
- // Glow effect layers/animation
- private let glowGradient = CAGradientLayer()
- // --- Create Record Sheet ---
- private var createRecordSheet: CreateRecordSheet?
- // 记账分类弹窗(快速记账)
- private var bookkeepingQuickSheet: BookkeepingQuickSheet?
-
- private var bookkeepingAmountSheet: BookkeepingAmountSheet?
-
- // Embedded bookkeeping page (keeps bottom tab visible)
- private var bookkeepingVC: BookkeepingViewController?
-
- // --- Pets (list & switching) ---
- private var pets: [PetSummary] = []
- private var currentPetIndex: Int = 0
- private let imageCache = NSCache<NSString, UIImage>()
-
- // --- Empty state & add pet ---
- private var hasPet: Bool = false
- private let addPetButton: UIButton = {
- let b = UIButton(type: .system)
- b.setTitle("添加+", for: .normal)
- b.setTitleColor(.black, for: .normal)
- b.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
- // b.contentEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18)
- return b
- }()
- private let emptyTipLabel: UILabel = {
- let l = UILabel()
- l.text = "还没有添加你的宠物哦~"
- l.font = .systemFont(ofSize: 16, weight: .medium)
- l.textColor = UIColor(hex: "#6B6B6B")
- l.textAlignment = .center
- return l
- }()
- private let emptyBanner: UIImageView = {
- let v = UIImageView()
- v.contentMode = .scaleToFill
- v.image = UIImage(named: "Home374")
- v.backgroundColor = UIColor(hex: "#FFF3D9")
- v.layer.cornerRadius = 12
- return v
- }()
-
- // MARK: - Pet names row
- private let petNamesScroll = UIScrollView()
- private let petNamesStack = UIStackView()
- private var petNameButtons: [UIButton] = []
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .white
- addTopGradient()
- layout()
- populateDemo()
- setHasPet(true)
-
- fetchMemberInfo()
- // Tap pet card to switch pets
- let petTap = UITapGestureRecognizer(target: self, action: #selector(showPetPicker))
- petCard.isUserInteractionEnabled = true
- petCard.addGestureRecognizer(petTap)
- }
- private func addTopGradient() {
- // 顶部淡米色渐变 -> 白色
- let g = CAGradientLayer()
- g.colors = [UIColor(hex: "#FFF3D9").cgColor, UIColor.white.cgColor]
- g.startPoint = CGPoint(x: 0.5, y: 0.0)
- g.endPoint = CGPoint(x: 0.5, y: 0.4)
- g.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 220)
- view.layer.insertSublayer(g, at: 0)
- }
- private func layout() {
- // Scroll
- scrollView.showsVerticalScrollIndicator = false
- view.addSubview(scrollView)
- scrollView.translatesAutoresizingMaskIntoConstraints = false
- contentView.translatesAutoresizingMaskIntoConstraints = false
- scrollView.addSubview(contentView)
- // Header
- [titleLabel, settingsButton, petCard, tagScroll, recordStack].forEach {
- $0.translatesAutoresizingMaskIntoConstraints = false
- contentView.addSubview($0)
- }
- // Empty state & add pet
- addPetButton.translatesAutoresizingMaskIntoConstraints = false
- emptyTipLabel.translatesAutoresizingMaskIntoConstraints = false
- emptyBanner.translatesAutoresizingMaskIntoConstraints = false
- contentView.addSubview(addPetButton)
- contentView.addSubview(emptyTipLabel)
- contentView.addSubview(emptyBanner)
- styleAddPetButton()
- addPetButton.addTarget(self, action: #selector(tapAddPet), for: .touchUpInside)
- // Tag row
- tagScroll.showsHorizontalScrollIndicator = false
- tagStack.axis = .horizontal
- tagStack.spacing = 12
- tagStack.alignment = .center
- tagScroll.addSubview(tagStack)
- tagStack.translatesAutoresizingMaskIntoConstraints = false
- // Records
- recordStack.axis = .vertical
- recordStack.spacing = 16
- // Bottom bar & add button
- view.addSubview(tabBarView)
- tabBarView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(addButton)
- addButton.translatesAutoresizingMaskIntoConstraints = false
-
-
- // —— 添加“所有宠物名字”的横向滚动条 —— //
- petNamesScroll.showsHorizontalScrollIndicator = false
- petNamesScroll.translatesAutoresizingMaskIntoConstraints = false
- contentView.addSubview(petNamesScroll)
- petNamesStack.axis = .horizontal
- petNamesStack.alignment = .center
- petNamesStack.spacing = 12
- petNamesStack.translatesAutoresizingMaskIntoConstraints = false
- petNamesScroll.addSubview(petNamesStack)
-
- NSLayoutConstraint.activate([
- // Scroll/Content
- scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- scrollView.bottomAnchor.constraint(equalTo: tabBarView.topAnchor),
- 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),
- // Header row
- titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
- titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
- settingsButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
- settingsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
- settingsButton.heightAnchor.constraint(equalToConstant: 28),
- settingsButton.widthAnchor.constraint(equalToConstant: 28),
- // Add Pet Button (below titleLabel)
- addPetButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
- addPetButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), addPetButton.heightAnchor.constraint(equalToConstant: 26),
- addPetButton.widthAnchor.constraint(equalToConstant: 64),
-
- // Pet names row
- petNamesScroll.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
- petNamesScroll.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
- petNamesScroll.trailingAnchor.constraint(equalTo: addPetButton.leadingAnchor, constant: -8),
- petNamesScroll.heightAnchor.constraint(equalToConstant: 26),
- petNamesStack.leadingAnchor.constraint(equalTo: petNamesScroll.leadingAnchor),
- petNamesStack.trailingAnchor.constraint(equalTo: petNamesScroll.trailingAnchor),
- petNamesStack.topAnchor.constraint(equalTo: petNamesScroll.topAnchor),
- petNamesStack.bottomAnchor.constraint(equalTo: petNamesScroll.bottomAnchor),
- // Pet card
- petCard.topAnchor.constraint(equalTo: petNamesScroll.bottomAnchor, constant: 17), petCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
- petCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
- petCard.heightAnchor.constraint(equalToConstant: 103),
-
- // Empty tip label (below petCard)
- emptyTipLabel.topAnchor.constraint(equalTo: petCard.bottomAnchor, constant: 137),
- emptyTipLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
- // Empty banner (below emptyTipLabel)
- emptyBanner.topAnchor.constraint(equalTo: emptyTipLabel.bottomAnchor, constant: 16),
- emptyBanner.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24),
- emptyBanner.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24),
- emptyBanner.heightAnchor.constraint(equalToConstant: 120),
- // Tag row
- tagScroll.topAnchor.constraint(equalTo: petCard.bottomAnchor, constant: 12),
- tagScroll.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
- tagScroll.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
- tagScroll.heightAnchor.constraint(equalToConstant: 38),
- tagStack.leadingAnchor.constraint(equalTo: tagScroll.leadingAnchor),
- tagStack.trailingAnchor.constraint(equalTo: tagScroll.trailingAnchor),
- tagStack.topAnchor.constraint(equalTo: tagScroll.topAnchor),
- tagStack.bottomAnchor.constraint(equalTo: tagScroll.bottomAnchor),
- tagStack.heightAnchor.constraint(equalTo: tagScroll.heightAnchor),
- // Records list
- recordStack.topAnchor.constraint(equalTo: tagScroll.bottomAnchor, constant: 12),
- recordStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
- recordStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
- recordStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -24),
- // Bottom tab bar
- tabBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- tabBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- tabBarView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
- // Add button (浮在中间)
- addButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- addButton.centerYAnchor.constraint(equalTo: tabBarView.topAnchor),
- addButton.widthAnchor.constraint(equalToConstant: 56),
- addButton.heightAnchor.constraint(equalToConstant: 56)
- ])
- // Overlay (dim background)
- overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
- overlayView.alpha = 0
- overlayView.isHidden = true
- overlayView.addTarget(self, action: #selector(hideActionMenu), for: .touchUpInside)
- view.insertSubview(overlayView, belowSubview: addButton)
- overlayView.translatesAutoresizingMaskIntoConstraints = false
- // Glow behind add button
- glowView.backgroundColor = UIColor.clear
- glowView.layer.cornerRadius = 28
- glowView.alpha = 0
- glowView.isHidden = true
- view.insertSubview(glowView, belowSubview: addButton)
- glowView.translatesAutoresizingMaskIntoConstraints = false
- // Configure glow gradient & shadow
- glowView.backgroundColor = .clear
- glowGradient.type = .radial
- // Neon glow color strictly FFE059
- glowGradient.colors = [
- UIColor(hex: "#FFE059").withAlphaComponent(0.95).cgColor,
- UIColor(hex: "#FFE059").withAlphaComponent(0.45).cgColor,
- UIColor(hex: "#FFE059").withAlphaComponent(0.0).cgColor
- ]
- glowGradient.locations = [0.0, 0.6, 1.0]
- // Keep glow gradient sized to the glowView
- glowGradient.frame = glowView.bounds
- glowGradient.cornerRadius = glowView.bounds.width/2
- glowView.layer.cornerRadius = glowView.bounds.width/2
- glowGradient.startPoint = CGPoint(x: 0.5, y: 0.5)
- glowGradient.endPoint = CGPoint(x: 1, y: 1)
- glowGradient.frame = glowView.bounds
- glowGradient.cornerRadius = glowView.bounds.width/2
- glowView.layer.insertSublayer(glowGradient, at: 0)
- // soft outer shadow
- glowView.layer.shadowColor = UIColor(hex: "#FFE059").cgColor
- glowView.layer.shadowOpacity = 0.9
- glowView.layer.shadowRadius = 24
- glowView.layer.shadowOffset = .zero
- glowGradient.frame = glowView.bounds
- glowGradient.cornerRadius = glowView.bounds.width/2
- // Action stacks
- func makeActionStack(button: UIButton, label: UILabel, systemImage: String, title: String) -> UIStackView {
- if let img = UIImage(named: systemImage) {
- button.setImage(img.withRenderingMode(.alwaysOriginal), for: .normal)
- } else {
- #if DEBUG
- print("⚠️ Image not found: \(systemImage)")
- #endif
- }
- button.imageView?.contentMode = .scaleAspectFit
- button.tintColor = .black
- button.backgroundColor = .white
- button.layer.cornerRadius = 28
- button.layer.shadowColor = UIColor.black.cgColor
- button.layer.shadowOpacity = 0.15
- button.layer.shadowRadius = 6
- button.layer.shadowOffset = CGSize(width: 0, height: 3)
- button.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- button.widthAnchor.constraint(equalToConstant: 56),
- button.heightAnchor.constraint(equalToConstant: 56)
- ])
- label.text = title
- label.font = .systemFont(ofSize: 12, weight: .medium)
- label.textColor = .white
- label.textAlignment = .center
- let stack = UIStackView(arrangedSubviews: [button, label])
- stack.axis = .vertical
- stack.alignment = .center
- stack.spacing = 8
- stack.alpha = 0
- stack.isHidden = true
- stack.translatesAutoresizingMaskIntoConstraints = false
- return stack
- }
- let leftStack = makeActionStack(button: leftActionButton, label: leftActionLabel, systemImage: "Home359", title: "事件")
- let rightStack = makeActionStack(button: rightActionButton, label: rightActionLabel, systemImage: "Home360", title: "记账")
- // leftActionStack.addArrangedSubview(leftActionButton) // keep references populated (optional)
- // rightActionStack.addArrangedSubview(rightActionButton)
- view.addSubview(leftStack)
- view.addSubview(rightStack)
- leftActionButton.addTarget(self, action: #selector(tapQuickEvent), for: .touchUpInside)
- rightActionButton.addTarget(self, action: #selector(tapQuickNote), for: .touchUpInside)
- NSLayoutConstraint.activate([
- overlayView.topAnchor.constraint(equalTo: view.topAnchor),
- overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- glowView.centerXAnchor.constraint(equalTo: addButton.centerXAnchor),
- glowView.centerYAnchor.constraint(equalTo: addButton.centerYAnchor),
- glowView.widthAnchor.constraint(equalToConstant: 64),
- glowView.heightAnchor.constraint(equalToConstant: 64),
- leftStack.centerXAnchor.constraint(equalTo: addButton.centerXAnchor, constant: -90),
- leftStack.bottomAnchor.constraint(equalTo: addButton.topAnchor, constant: -24),
- rightStack.centerXAnchor.constraint(equalTo: addButton.centerXAnchor, constant: 90),
- rightStack.bottomAnchor.constraint(equalTo: addButton.topAnchor, constant: -24)
- ])
- // Store stacks for later animations by tagging via accessibilityIdentifier
- leftStack.accessibilityIdentifier = "leftActionStack"
- rightStack.accessibilityIdentifier = "rightActionStack"
- // 注册宠物保存通知
- NotificationCenter.default.addObserver(self, selector: #selector(handlePetDidSave(_:)), name: .petDidSave, object: nil)
- NotificationCenter.default.addObserver(self, selector: #selector(handleEventRecordCreated(_:)), name: .eventRecordDidCreate, object: nil)
- }
-
- @objc private func handleEventRecordCreated(_ note: Notification) {
- // 优先取通知里传来的 petId;否则用当前选中的宠物
- let pid = (note.userInfo?["petId"] as? String) ?? (currentPetIndex < pets.count ? pets[currentPetIndex].id : "")
- guard !pid.isEmpty else { return }
- // 刷新分类与事件记录(默认展示全部)
- fetchRecordCategories(petId: pid)
- fetchEventRecords(petId: pid, keyword: "")
- }
-
- @objc private func handlePetDidSave(_ note: Notification) {
- // Prefer userId from notification; fallback to persisted value
- let uidFromNote = note.userInfo?["userId"] as? String
- let persistedStr = UserDefaults.standard.string(forKey: "userId")
- let persistedInt = UserDefaults.standard.integer(forKey: "userId")
- let uid = uidFromNote ?? persistedStr ?? (persistedInt == 0 ? "" : String(persistedInt))
- guard !uid.isEmpty else { return }
- // 重新拉取宠物列表并刷新UI
- fetchPetsForUser(userId: uid)
- }
-
- @objc private func tapAddPet() {
- let vc = AddPetViewController()
- navigationController?.pushViewController(vc, animated: true)
- }
- private func styleAddPetButton() {
- addPetButton.backgroundColor = UIColor(hex: "#FFE059")
- addPetButton.layer.cornerRadius = 13
- addPetButton.clipsToBounds = true
- addPetButton.setTitleColor(.black, for: .normal)
- addPetButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .regular)
- // addPetButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 18, bottom: 0, right: 18)
- }
- private func setHasPet(_ value: Bool) {
- hasPet = value
- // addPetButton.isHidden = value
- tagScroll.isHidden = !value
- recordStack.isHidden = !value
- emptyTipLabel.isHidden = value
- emptyBanner.isHidden = value
- if !value {
- petCard.configure(
- avatar: UIImage(named: "Home372")!,
- name: "添加我的宠物",
- breed: "品种",
- ageText: "年龄",
- daysText: "一起生活的第XX天"
- )
- }
- // If true, do not touch petCard (keep current configuration)
- }
- private func populateDemo() {
- // Pet card demo
- petCard.configure(
- avatar: UIImage(named: "Home372") ?? UIImage(systemName: "pawprint")!,
- name: "宠物名字",
- breed: "宠物品种",
- ageText: "XX岁",
- daysText: "一起生活的第XX天"
- )
-
- tabBarView.onSelect = { [weak self] idx in
- guard let self = self else { return }
- if idx == 0 {
- // 事件:显示首页,关闭记账浮层
- self.hideBookkeeping()
- let topY = -self.scrollView.adjustedContentInset.top
- self.scrollView.setContentOffset(CGPoint(x: 0, y: topY), animated: true)
- } else {
- // 记账:作为子控制器叠加在内容区域(tabbar 之上)
- self.showBookkeeping()
- }
- }
- }
- @objc private func tapSettings() {
- print("Settings tapped")
- let vc = SettingsViewController()
- navigationController?.pushViewController(vc, animated: true)
-
- }
- @objc private func tapAdd() {
- isActionMenuShown ? hideActionMenu() : showActionMenu()
- }
- private func actionStacks() -> [UIStackView] {
- return view.subviews.compactMap { ($0 as? UIStackView)?.accessibilityIdentifier?.contains("ActionStack") == true ? ($0 as! UIStackView) : nil }
- }
- private func findStack(id: String) -> UIStackView? {
- view.subviews.first { ($0 as? UIStackView)?.accessibilityIdentifier == id } as? UIStackView
- }
- private func showActionMenu() {
- guard let leftStack = findStack(id: "leftActionStack"), let rightStack = findStack(id: "rightActionStack") else { return }
- isActionMenuShown = true
- overlayView.isHidden = false
- glowView.isHidden = false
- glowGradient.frame = glowView.bounds
- leftStack.isHidden = false
- rightStack.isHidden = false
- addButton.setImage(UIImage(named: "Home358"), for: .normal)
- startGlowPulse()
- view.layoutIfNeeded()
- leftStack.transform = CGAffineTransform(translationX: 0, y: 20)
- rightStack.transform = CGAffineTransform(translationX: 0, y: 20)
- UIView.animate(withDuration: 0.22) {
- self.overlayView.alpha = 1
- self.glowView.alpha = 1
- leftStack.alpha = 1; rightStack.alpha = 1
- leftStack.transform = .identity
- rightStack.transform = .identity
- }
- }
- @objc private func hideActionMenu() {
- guard let leftStack = findStack(id: "leftActionStack"), let rightStack = findStack(id: "rightActionStack") else { return }
- UIView.animate(withDuration: 0.2, animations: {
- self.overlayView.alpha = 0
- self.glowView.alpha = 0
- leftStack.alpha = 0; rightStack.alpha = 0
- leftStack.transform = CGAffineTransform(translationX: 0, y: 10)
- rightStack.transform = CGAffineTransform(translationX: 0, y: 10)
- }, completion: { _ in
- self.overlayView.isHidden = true
- leftStack.isHidden = true
- rightStack.isHidden = true
- self.addButton.setImage(UIImage(named: "Home357"), for: .normal)
- self.isActionMenuShown = false
- self.glowView.isHidden = true
- self.glowGradient.removeAllAnimations()
- self.stopGlowPulse()
- })
- }
- @objc private func tapQuickEvent() {
- hideActionMenu()
- showCreateRecordSheet()
- }
- @objc private func tapQuickNote() {
- print("快速进入 记账")
- hideActionMenu()
- showBookkeepingQuickSheet()
- }
-
- private func startGlowPulse() {
- if glowGradient.animation(forKey: "pulse") != nil { return }
- let scale = CABasicAnimation(keyPath: "transform.scale")
- scale.fromValue = 0.95
- scale.toValue = 1.15
- scale.duration = 1.1
- scale.autoreverses = true
- scale.repeatCount = .infinity
- let opacity = CABasicAnimation(keyPath: "opacity")
- opacity.fromValue = 0.7
- opacity.toValue = 1.0
- opacity.duration = 1.1
- opacity.autoreverses = true
- opacity.repeatCount = .infinity
- let group = CAAnimationGroup()
- group.animations = [scale, opacity]
- group.duration = 1.1
- group.autoreverses = true
- group.repeatCount = .infinity
- glowGradient.add(group, forKey: "pulse")
- }
- private func stopGlowPulse() {
- glowGradient.removeAnimation(forKey: "pulse")
- }
-
- // MARK: - Embed/Remove Bookkeeping (child VC keeps bottom tab visible)
- private func showBookkeeping() {
- // 已经展示就跳过
- if let _ = bookkeepingVC { return }
- let vc = BookkeepingViewController()
- addChild(vc)
- view.insertSubview(vc.view, belowSubview: overlayView)
- vc.view.translatesAutoresizingMaskIntoConstraints = false
- // 占据安全区顶部到 tabBar 顶部之间的区域(不遮挡底部导航)
- NSLayoutConstraint.activate([
- vc.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- vc.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- vc.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- vc.view.bottomAnchor.constraint(equalTo: tabBarView.topAnchor)
- ])
- // 先确保 view 已加载,再设置宠物
- vc.loadViewIfNeeded()
- vc.setPets(pets.map { ($0.id, $0.name) }, selectedIndex: currentPetIndex)
- // keep "+"、glow、action stacks and overlay above bookkeeping
- self.view.bringSubviewToFront(self.overlayView)
- self.view.bringSubviewToFront(self.addButton)
- self.view.bringSubviewToFront(self.glowView)
- if let l = self.findStack(id: "leftActionStack") { self.view.bringSubviewToFront(l) }
- if let r = self.findStack(id: "rightActionStack") { self.view.bringSubviewToFront(r) }
- vc.didMove(toParent: self)
- bookkeepingVC = vc
- // 可选:淡入
- vc.view.alpha = 0
- UIView.animate(withDuration: 0.22) { vc.view.alpha = 1 }
- }
- private func hideBookkeeping() {
- guard let vc = bookkeepingVC else { return }
- // 可选:淡出
- UIView.animate(withDuration: 0.18, animations: { vc.view.alpha = 0 }) { _ in
- vc.willMove(toParent: nil)
- vc.view.removeFromSuperview()
- vc.removeFromParent()
- self.bookkeepingVC = nil
- }
- }
-
- // MARK: - Create Record Sheet (Custom Modal)
- private func showCreateRecordSheet() {
- if createRecordSheet == nil {
- let sheet = CreateRecordSheet()
- sheet.viewController = self
- createRecordSheet = sheet
- view.addSubview(sheet)
- sheet.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- sheet.topAnchor.constraint(equalTo: view.topAnchor),
- sheet.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- sheet.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- sheet.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- // Ensure the sheet is above everything (including bookkeeping)
- self.view.bringSubviewToFront(sheet)
- }
- createRecordSheet?.show()
- }
- // MARK: - Quick Bookkeeping Sheet (弹窗)
- private func showBookkeepingQuickSheet() {
- if bookkeepingQuickSheet == nil {
- let s = BookkeepingQuickSheet()
- // 选中某个记账分类后的回调(后续你可以在这里跳转到“新增记账”编辑页、带上已选分类)
- s.onItemSelected = { [weak self] name in
- print("选择记账分类: \(name)")
- // 点击分类后,叠加展示金额输入弹窗
- self?.showBookkeepingAmountSheet(category: name)
- }
- s.onDismiss = { [weak self] in
- // 可留空;若想释放:self?.bookkeepingQuickSheet = nil
- }
- bookkeepingQuickSheet = s
- view.addSubview(s)
- s.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- s.topAnchor.constraint(equalTo: view.topAnchor),
- s.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- s.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- s.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- }
- if let s = bookkeepingQuickSheet {
- // 确保在最上层(不被子控制器等覆盖)
- view.bringSubviewToFront(s)
- s.show()
- }
- }
-
- private func showBookkeepingAmountSheet(category: String) {
- if bookkeepingAmountSheet == nil {
- let s = BookkeepingAmountSheet()
- // 日期选择:优先用你的 DatePickerSheetController;没有就用内置 actionSheet
- s.onPickDate = { [weak self] current, handler in
- guard let self = self else { return }
- if let cls = NSClassFromString("DatePickerSheetController") as? UIViewController.Type,
- let picker = (cls as? NSObject.Type)?.init() as? UIViewController {
- // 如果你项目里的 DatePickerSheetController 对外有回调 onConfirm(Date)
- picker.setValue({ (d: Date) in
- handler(d)
- picker.dismiss(animated: true)
- }, forKey: "onConfirm")
- picker.setValue(current, forKey: "initialDate")
- self.present(picker, animated: true)
- } else {
- // 兜底:系统 UIDatePicker
- let ac = UIAlertController(title: "\n\n\n\n\n\n\n\n\n", message: nil, preferredStyle: .actionSheet)
- ac.modalPresentationStyle = .popover
- if let p = ac.popoverPresentationController{
- // 锚到视图底部中间;去掉箭头,效果更像底部弹出
- p.sourceView = self.view
- p.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.maxY - 1, width: 1, height: 1)
- p.permittedArrowDirections = []
- p.delegate = self
- }
-
- let dp = UIDatePicker(frame: .zero)
- dp.datePickerMode = .date
- if #available(iOS 13.4, *) {
- dp.preferredDatePickerStyle = .wheels
- } else {
- // Fallback on earlier versions
- }
- dp.date = current
- ac.view.addSubview(dp)
- dp.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- dp.centerXAnchor.constraint(equalTo: ac.view.centerXAnchor),
- dp.topAnchor.constraint(equalTo: ac.view.topAnchor, constant: 8)
- ])
- ac.addAction(UIAlertAction(title: "确定", style: .default, handler: { _ in handler(dp.date) }))
- ac.addAction(UIAlertAction(title: "取消", style: .cancel))
- self.present(ac, animated: true)
- }
- }
- // 宠物选择:用当前拿到的 pets 做一个 ActionSheet
- s.onPickPet = { [weak self] handler in
- guard let self = self else { return }
- guard !self.pets.isEmpty else { return }
- let ac = UIAlertController(title: "选择宠物", message: nil, preferredStyle: .actionSheet)
- ac.modalPresentationStyle = .popover
- if let p = ac.popoverPresentationController{
- p.sourceView = self.view
- p.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.maxY - 1, width: 1, height: 1)
- p.permittedArrowDirections = []
- p.delegate = self
- }
-
- for p in self.pets {
- ac.addAction(UIAlertAction(title: p.name, style: .default, handler: { _ in handler(p) }))
- }
- ac.addAction(UIAlertAction(title: "取消", style: .cancel))
- self.present(ac, animated: true)
- }
- // 保存:发POST到后端
- s.onSave = { [weak self] cate, date, pet, note, amountText in
- guard let self = self else { return }
- // 1) 解析 petId
- let current = (self.currentPetIndex >= 0 && self.currentPetIndex < self.pets.count) ? self.pets[self.currentPetIndex] : nil
- let usePet = pet ?? current
- guard let petIdStr = usePet?.id, let petId = Int(petIdStr) else {
- print("❌ 缺少 petId,无法提交")
- return
- }
- // 2) 金额
- let amt = Double(amountText.replacingOccurrences(of: ",", with: "")) ?? 0
- // 3) recordTypeId & 日期
- let typeId = self.recordTypeId(for: cate)
- let dateStr = self.formatSendDate(date)
- // 4) 组装请求
- guard let url = URL(string: "\(baseURL)/petRecordInfo/createRecord") 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")
- }
- let body: [String: Any] = [
- "amount": amt,
- "note": note,
- "petId": petId,
- "recordDate": dateStr,
- "recordTypeId": typeId,
- "title": cate,
- "module": "account"
- ]
- do {
- req.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
- } catch {
- print("❌ 序列化失败: \(error)")
- return
- }
- URLSession.shared.dataTask(with: req) { [weak self] data, resp, err in
- if let http = resp as? HTTPURLResponse { print("📮 createRecord status: \(http.statusCode)") }
- if let err = err { print("❌ createRecord error: \(err)"); return }
- guard let data = data else { print("❌ createRecord 空响应"); return }
- if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
- print("✅ createRecord 响应: \(json)")
- }
- // 成功后可刷新当前宠物的列表(如果你希望记账列表也依赖这个接口)
- DispatchQueue.main.async {
- // ✅ 通知记账页刷新(携带 petId / 日期 / 类目 / 金额)
- NotificationCenter.default.post(
- name: .bookkeepingRecordDidCreate,
- object: nil,
- userInfo: [
- "petId": String(petId),
- "recordDate": dateStr,
- "title": cate,
- "amount": amt
- ]
- )
- }
- }.resume()
- }
- bookkeepingAmountSheet = s
- view.addSubview(s)
- s.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- s.topAnchor.constraint(equalTo: view.topAnchor),
- s.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- s.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- s.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- }
- if let s = bookkeepingAmountSheet {
- // 传入当前选中的宠物
- let current = (currentPetIndex >= 0 && currentPetIndex < pets.count) ? pets[currentPetIndex] : nil
- s.configure(category: category, currentPet: current)
- view.bringSubviewToFront(s) // 放到最上层
- s.show()
- }
- }
- private func formatSendDate(_ d: Date) -> String {
- let f = DateFormatter()
- f.locale = Locale(identifier: "zh_CN")
- f.dateFormat = "yyyy-MM-dd"
- return f.string(from: d)
- }
- private func recordTypeId(for title: String) -> Int {
- // 记账(module: account)映射——按后端给的 id
- // 为了兼容不同文案,做了少量同义词映射(如“草粮”→25)
- let map: [String: Int] = [
- // 食物 21~25
- "干粮": 21,
- "罐头": 22,
- "零食": 23,
- "冻干": 24,
- "草根": 25,
- // 生活 26~30
- "衣服": 26,
- "玩具": 27,
- "餐具": 28,
- "厕所": 29,
- "牵引绳": 30,
- // 治病 31~35
- "驱虫药": 31,
- "保健品": 32,
- "手术": 33,
- "药品": 34,
- "疫苗": 35,
- // 清洁 36~40
- "指甲剪": 36,
- "梳子": 37,
- "清洁用品": 38,
- "洗澡美容": 39,
- "洗护用品": 40
- ]
- return map[title] ?? 0
- }
-
- // MARK: - Record Categories (single-select with "全部")
- private var currentCategories: [String] = []
- private var categoryPills: [DottedPill] = []
- private var selectedCategoryIndex: Int = 0 // 0 == 全部
- // MARK: - Member Info Networking
- private func fetchMemberInfo() {
- guard let url = URL(string: "\(baseURL)/petRecordApUser/info") 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")
- }
- let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let error = error {
- print("Error: \(error)")
- return
- }
- guard let data = data else { return }
- do {
- let decoder = JSONDecoder()
- let responseObject = try decoder.decode(MemberInfoResponse.self, from: data)
- if responseObject.code == "200" {
- // Store the data
- self?.storeMemberInfo(responseObject.data)
- } else {
- print("Error: \(responseObject.msg ?? "Unknown error")")
- }
- } catch {
- print("Error decoding response: \(error)")
-
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
- // 切换到登录界面
- self?.switchToLoginScreen()
- }
-
- }
- }
- task.resume()
- }
-
- private func switchToLoginScreen() {
-
- // 清除用户登录状态
- UserDefaults.standard.set(false, forKey: "isLogggedIn")
- UserDefaults.standard.removeObject(forKey: "userToken")
- // 创建登录界面
- let loginVC = LoginViewController()
- let navController = UINavigationController(rootViewController: loginVC)
-
- // 获取当前窗口
- guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
- return
- }
-
- // 设置根视图控制器为登录界面
- window.rootViewController = navController
-
- // 添加切换动画
- UIView.transition(with: window,
- duration: 0.4,
- options: .transitionCrossDissolve,
- animations: nil,
- completion: nil)
- }
- private func storeMemberInfo(_ member: MemberInfo) {
-
- print("会员信息:")
- print("UserID: \(member.userId)")
- print("Phone: \(member.memberPhone)")
- print("Name: \(member.memberName)")
- print("Icon: \(member.memberIcon ?? "No Icon")")
- print("Email: \(member.memberEmail ?? "No Email")")
- print("Created Time: \(member.createdTime)")
- print("Updated Time: \(member.updatedTime)")
- if let token = UserDefaults.standard.string(forKey: "userToken") {
- print("token: \(token)")
- }
- // Store data in UserDefaults or another storage solution
- UserDefaults.standard.set(member.userId, forKey: "userId")
- UserDefaults.standard.set(member.memberPhone, forKey: "memberPhone")
- UserDefaults.standard.set(member.memberName, forKey: "memberName")
- UserDefaults.standard.set(member.memberIcon, forKey: "memberIcon")
- UserDefaults.standard.set(member.memberEmail, forKey: "memberEmail")
- UserDefaults.standard.set(member.createdTime, forKey: "createdTime")
- UserDefaults.standard.set(member.updatedTime, forKey: "updatedTime")
- // After we have userId, fetch pets for this user
- DispatchQueue.main.async { [weak self] in
- self?.fetchPetsForUser(userId: member.userId)
- }
- }
- // MARK: - Pets networking & UI
- private func fetchPetsForUser(userId: String) {
- var components = URLComponents(string: "\(baseURL)/petRecordPet/queryPetList")
- components?.queryItems = [URLQueryItem(name: "userId", value: userId)]
- guard let url = components?.url else { return }
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- if let token = UserDefaults.standard.string(forKey: "userToken") {
- request.setValue(token, forHTTPHeaderField: "Authorization")
- }
- print("🐾 GET: \(url.absoluteString)")
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse { print("🐾 status: \(http.statusCode)") }
- if let error = error { print("🐾 pets error: \(error)"); return }
- guard let data = data else { print("🐾 pets empty data"); return }
- do {
- let decoder = JSONDecoder()
- let resp = try decoder.decode(PetListByUserResponse.self, from: data)
- if resp.code == "200" {
- DispatchQueue.main.async {
- self?.pets = resp.data
- if let first = resp.data.first {
- self?.currentPetIndex = 0
- self?.buildPetNameChips()
- self?.applyPet(first)
- }
- }
- } else {
- print("🐾 pets server msg: \(resp.msg ?? "-") code=\(resp.code)")
- }
- } catch {
- print("🐾 pets decode error: \(error)")
- }
- }.resume()
- }
- private func applyPet(_ pet: PetSummary) {
- let breed = pet.breedName ?? "宠物品种"
- let ageText: String = {
- if let a = pet.age, !a.isEmpty { return "\(a)岁" }
- return "年龄"
- }()
- let days: String = {
- if let d = pet.togetherDays, !d.isEmpty { return "一起生活的第\(d)天" }
- return "一起生活的第XX天"
- }()
- let placeholder = UIImage(named: "Home372") ?? UIImage(systemName: "pawprint")!
- if let avatar = pet.avatar, let url = URL(string: avatar) {
- loadImage(from: url) { [weak self] img in
- self?.petCard.configure(
- avatar: img ?? placeholder,
- name: pet.name,
- breed: breed,
- ageText: ageText,
- daysText: days
- )
- }
- } else {
- petCard.configure(
- avatar: placeholder,
- name: pet.name,
- breed: breed,
- ageText: ageText,
- daysText: days
- )
- }
- updatePetNameSelection()
- // 拉取该宠物的记录分类(填充到标签行)
- fetchRecordCategories(petId: pet.id)
-
- // 拉取事件记录列表(默认全部)
- fetchEventRecords(petId: pet.id, keyword: "")
- }
- // 拉取记录分类列表(入参 petId)并渲染到 tag 行
- private func fetchRecordCategories(petId: String) {
- var comps = URLComponents(string: "\(baseURL)/petRecordInfo/queryRecordListCategory")
- comps?.queryItems = [URLQueryItem(name: "petId", value: petId)]
- guard let url = comps?.url else { return }
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- if let token = UserDefaults.standard.string(forKey: "userToken") {
- request.setValue(token, forHTTPHeaderField: "Authorization")
- }
- print("🏷️ GET: \(url.absoluteString)")
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse { print("🏷️ status: \(http.statusCode)") }
- if let error = error { print("🏷️ category error: \(error)"); return }
- guard let data = data else { print("🏷️ category empty data"); return }
- do {
- let decoder = JSONDecoder()
- let resp = try decoder.decode(RecordCategoriesResponse.self, from: data)
- if resp.code == "200" {
- DispatchQueue.main.async {
- self?.populateTagCategories(resp.data)
- }
- } else {
- print("🏷️ server msg: \(resp.msg ?? "-") code=\(resp.code)")
- }
- } catch {
- print("🏷️ decode error: \(error)")
- }
- }.resume()
- }
- // 用从接口拿到的分类填充 UI
- private func populateTagCategories(_ categories: [String]) {
- // 缓存数据
- currentCategories = categories
- // 清空旧的
- categoryPills.removeAll()
- tagStack.arrangedSubviews.forEach { v in
- tagStack.removeArrangedSubview(v)
- v.removeFromSuperview()
- }
- // 在最前面加上 “全部”
- let items: [String] = ["全部"] + categories
- // 永远展示(至少有“全部”)
- tagScroll.isHidden = items.isEmpty
- guard !items.isEmpty else { return }
- // 默认选中“全部”
- selectedCategoryIndex = min(selectedCategoryIndex, items.count - 1)
- for (idx, name) in items.enumerated() {
- let pill = DottedPill(text: name)
- pill.isUserInteractionEnabled = true
- pill.tag = idx
- // 选中态样式
- pill.setSelected(idx == selectedCategoryIndex)
- // 点击切换(单选)
- let tap = UITapGestureRecognizer(target: self, action: #selector(tapCategoryPill(_:)))
- pill.addGestureRecognizer(tap)
- pill.translatesAutoresizingMaskIntoConstraints = false
- tagStack.addArrangedSubview(pill)
- categoryPills.append(pill)
- }
- }
- // MARK: - Event Records networking & UI
- private func fetchEventRecords(petId: String, keyword: String) {
- var comps = URLComponents(string: "\(baseURL)/petRecordInfo/queryEventRecordInfoList")
- comps?.queryItems = [
- URLQueryItem(name: "petId", value: petId),
- URLQueryItem(name: "keyword", value: keyword)
- ]
- guard let url = comps?.url else { return }
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- if let token = UserDefaults.standard.string(forKey: "userToken") {
- request.setValue(token, forHTTPHeaderField: "Authorization")
- }
- print("🗒️ GET: \(url.absoluteString)")
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse { print("🗒️ status: \(http.statusCode)") }
- if let error = error { print("🗒️ records error: \(error)"); return }
- guard let data = data else { print("🗒️ records empty data"); return }
- do {
- let decoder = JSONDecoder()
- let resp = try decoder.decode(EventRecordListResponse.self, from: data)
- if resp.code == "200" {
- DispatchQueue.main.async { self?.renderRecords(resp.data) }
- } else {
- print("🗒️ records server msg: \(resp.msg ?? "-") code=\(resp.code)")
- DispatchQueue.main.async { self?.renderRecords([]) }
- }
- } catch {
- print("🗒️ records decode error: \(error)")
- DispatchQueue.main.async { self?.renderRecords([]) }
- }
- }.resume()
- }
- private func renderRecords(_ list: [EventRecord]) {
- // 清空旧列表
- recordStack.arrangedSubviews.forEach { v in
- recordStack.removeArrangedSubview(v)
- v.removeFromSuperview()
- }
- guard !list.isEmpty else { return }
- let placeholder = UIImage(named: "peihead") ?? UIImage(systemName: "pawprint.circle")!
- for rec in list {
- let card = RecordCard()
- // 填充内容
- let dateText = formatAPIDate(rec.recordDate)
- let summary = (rec.content?.isEmpty == false) ? rec.content! : (rec.note ?? "")
- card.configure(
- avatar: placeholder,
- title: rec.title,
- date: dateText,
- badge: rec.petName,
- summary: summary
- )
- // 异步加载头像
- // if let urlStr = rec.avatar, let url = URL(string: urlStr) {
- // loadImage(from: url) { img in
- // // 简单重配一次以更新图片
- // card.configure(
- // avatar: img ?? placeholder,
- // title: rec.title,
- // date: dateText,
- // badge: rec.petName,
- // summary: summary
- // )
- // }
- // }
- recordStack.addArrangedSubview(card)
- }
- }
- private func formatAPIDate(_ s: String?) -> String {
- guard let s = s, !s.isEmpty else { return "" }
- let inFmt = DateFormatter()
- inFmt.locale = Locale(identifier: "en_US_POSIX")
- inFmt.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" // 例: 2025-08-26T00:00:00.000+08:00
- if let date = inFmt.date(from: s) {
- let out = DateFormatter()
- out.locale = Locale(identifier: "zh_CN")
- out.dateFormat = "yyyy.MM.dd"
- return out.string(from: date)
- }
- return ""
- }
-
- // 构建“所有宠物名字”的一排按钮
- private func buildPetNameChips() {
- // 清空旧内容
- petNameButtons.removeAll()
- petNamesStack.arrangedSubviews.forEach { v in
- petNamesStack.removeArrangedSubview(v)
- v.removeFromSuperview()
- }
- for (idx, p) in pets.enumerated() {
- let btn = UIButton(type: .system)
- btn.setTitle(p.name, for: .normal)
- btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
- btn.setTitleColor(UIColor(hex: "#2B2B2B"), for: .normal)
- btn.backgroundColor = .clear
- btn.contentEdgeInsets = UIEdgeInsets(top: 4, left: 10, bottom: 4, right: 10)
- btn.layer.cornerRadius = 6
- btn.layer.masksToBounds = true
- btn.tag = idx
- btn.addTarget(self, action: #selector(tapPetNameButton(_:)), for: .touchUpInside)
- petNamesStack.addArrangedSubview(btn)
- petNameButtons.append(btn)
- }
- updatePetNameSelection()
- }
- private func updatePetNameSelection() {
- for (i, b) in petNameButtons.enumerated() {
- let selected = (i == currentPetIndex)
- if selected {
- b.backgroundColor = UIColor(hex: "#FFE059")
- b.setTitleColor(.black, for: .normal)
- } else {
- b.backgroundColor = .clear
- b.setTitleColor(UIColor(hex: "#2B2B2B"), for: .normal)
- }
- }
- }
- @objc private func tapCategoryPill(_ g: UITapGestureRecognizer) {
- guard let pill = g.view as? DottedPill else { return }
- let idx = pill.tag
- guard idx != selectedCategoryIndex, idx >= 0, idx < categoryPills.count else { return }
- // 单选:只允许一个选中
- selectedCategoryIndex = idx
- for (i, p) in categoryPills.enumerated() {
- p.setSelected(i == selectedCategoryIndex)
- }
- // 触发记录过滤(keyword 为空表示全部)
- let keyword = (idx == 0) ? "" : currentCategories[idx - 1]
- let currentPetId = (currentPetIndex >= 0 && currentPetIndex < pets.count) ? pets[currentPetIndex].id : ""
- guard !currentPetId.isEmpty else { return }
- fetchEventRecords(petId: currentPetId, keyword: keyword)
- }
-
- @objc private func tapPetNameButton(_ sender: UIButton) {
- let idx = sender.tag
- guard idx >= 0, idx < pets.count else { return }
- currentPetIndex = idx
- applyPet(pets[idx]) // 切换卡片信息
- updatePetNameSelection() // 同步高亮
- }
- @objc private func showPetPicker() {
- guard !pets.isEmpty else { return }
- let ac = UIAlertController(title: "选择宠物", message: nil, preferredStyle: .actionSheet)
- // ✅ iPad 必须设置 popover 锚点
- ac.modalPresentationStyle = .popover
- if let p = ac.popoverPresentationController {
- p.sourceView = petCard
- p.sourceRect = petCard.bounds
- p.permittedArrowDirections = .up
- p.delegate = self
- }
- for (idx, p) in pets.enumerated() {
- ac.addAction(UIAlertAction(title: p.name, style: .default, handler: { [weak self] _ in
- self?.currentPetIndex = idx
- self?.applyPet(p)
- }))
- }
- ac.addAction(UIAlertAction(title: "取消", style: .cancel))
- present(ac, animated: true)
- }
-
- private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
- let key = url.absoluteString as NSString
- if let cached = imageCache.object(forKey: key) {
- completion(cached)
- return
- }
- URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
- var img: UIImage? = nil
- if let data = data { img = UIImage(data: data) }
- if let img = img { self?.imageCache.setObject(img, forKey: key) }
- DispatchQueue.main.async { completion(img) }
- }.resume()
- }
- // Create a struct to model the response
- struct MemberInfoResponse: Codable {
- let code: String
- let msg: String?
- let data: MemberInfo
- }
- struct MemberInfo: Codable {
- let userId: String
- let memberPhone: String
- let memberName: String
- let memberIcon: String?
- let memberEmail: String?
- let status: String?
- let createdTime: String
- let updatedTime: String
- }
- // Flexible code type (string or int)
- enum CodeValue: Codable {
- case string(String)
- case int(Int)
- init(from decoder: Decoder) throws {
- let c = try decoder.singleValueContainer()
- if let s = try? c.decode(String.self) { self = .string(s) }
- else if let i = try? c.decode(Int.self) { self = .int(i) }
- else { self = .string("") }
- }
- func equals200() -> Bool {
- switch self { case .string(let s): return s == "200"; case .int(let i): return i == 200 }
- }
- var rawString: String { switch self { case .string(let s): return s; case .int(let i): return String(i) } }
- }
- struct PetListByUserResponse: Codable {
- let code: String
- let msg: String?
- let data: [PetSummary]
- }
- struct PetSummary: Codable {
- let id: String
- let userId: String
- let name: String
- let nickname: String?
- let weight: String?
- let categoryId: String?
- let categoryName: String?
- let breedId: String?
- let breedName: String?
- let gender: String?
- let birthDate: String?
- let arrivalDate: String?
- let avatar: String?
- let age: String?
- let togetherDays: String?
- }
- // 拉取记录分类接口响应
- struct RecordCategoriesResponse: Codable {
- let code: String
- let msg: String?
- let data: [String]
- }
- // 拉取事件记录列表响应
- struct EventRecordListResponse: Codable {
- let code: String
- let msg: String?
- let data: [EventRecord]
- }
- struct EventRecord: Codable {
- let id: String
- let petId: String
- let avatar: String?
- let petName: String
- let recordTypeId: String?
- let recordUrl: String?
- let title: String
- let recordDate: String?
- let content: String?
- let amount: String?
- let note: String?
- let module: String?
- }
- // MARK: - UIPopoverPresentationControllerDelegate (fallback)
- func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
- if popoverPresentationController.sourceView == nil && popoverPresentationController.barButtonItem == nil {
- popoverPresentationController.sourceView = view
- popoverPresentationController.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY - 1, width: 1, height: 1)
- popoverPresentationController.permittedArrowDirections = []
- }
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self, name: .petDidSave, object: nil)
- }
- }
- // MARK: - PetDidSave Notification Name
- extension Notification.Name {
- static let petDidSave = Notification.Name("PetDidSave")
- // 已有类似事件通知就保留,这里新增记账创建成功通知
- static let bookkeepingRecordDidCreate = Notification.Name("BookkeepingRecordDidCreate")
- }
- // MARK: - PetHeaderCard
- final class PetHeaderCard: UIView {
- private let container = UIImageView()
- private let avatarView = UIImageView()
- private let nameLabel = UILabel()
- private let chipStack = UIStackView()
- private let daysRibbon = PaddingLabel(insets: .init(top: 6, left: 12, bottom: 6, right: 12))
- override init(frame: CGRect) {
- super.init(frame: frame)
- translatesAutoresizingMaskIntoConstraints = false
- container.translatesAutoresizingMaskIntoConstraints = false
- container.contentMode = .scaleToFill
- container.image = UIImage(named: "Home377")
- container.layer.shadowColor = UIColor.black.cgColor
- container.layer.shadowOpacity = 0.07
- container.layer.shadowRadius = 8
- container.layer.shadowOffset = .init(width: 0, height: 2)
- addSubview(container)
- avatarView.contentMode = .scaleAspectFill
- avatarView.layer.cornerRadius = 32
- avatarView.clipsToBounds = true
- avatarView.backgroundColor = UIColor(hex: "#FFF3D9")
- nameLabel.font = .systemFont(ofSize: 15, weight: .semibold)
- nameLabel.textColor = UIColor(hex: "#2B2B2B")
- chipStack.axis = .horizontal
- chipStack.spacing = 8
- chipStack.alignment = .center
- daysRibbon.backgroundColor = UIColor(hex: "#FFE059")
- daysRibbon.textColor = .black
- daysRibbon.font = .systemFont(ofSize: 10, weight: .medium)
- daysRibbon.layer.cornerRadius = 14
- daysRibbon.layer.masksToBounds = true
- [avatarView, nameLabel, chipStack, daysRibbon].forEach {
- $0.translatesAutoresizingMaskIntoConstraints = false
- container.addSubview($0)
- }
- NSLayoutConstraint.activate([
- container.topAnchor.constraint(equalTo: topAnchor),
- container.leadingAnchor.constraint(equalTo: leadingAnchor),
- container.trailingAnchor.constraint(equalTo: trailingAnchor),
- container.bottomAnchor.constraint(equalTo: bottomAnchor),
- avatarView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
- avatarView.centerYAnchor.constraint(equalTo: container.centerYAnchor),
- avatarView.widthAnchor.constraint(equalToConstant: 64),
- avatarView.heightAnchor.constraint(equalToConstant: 64),
- nameLabel.topAnchor.constraint(equalTo: avatarView.topAnchor, constant: 0),
- nameLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 10),
- nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: daysRibbon.leadingAnchor, constant: -8),
- chipStack.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
- chipStack.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
- chipStack.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12),
- daysRibbon.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
- daysRibbon.topAnchor.constraint(equalTo: container.topAnchor, constant: 8)
- ])
- }
- required init?(coder: NSCoder) { fatalError() }
- func configure(avatar: UIImage, name: String, breed: String, ageText: String, daysText: String) {
- avatarView.image = avatar
- nameLabel.text = name
- chipStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
- [breed, ageText].forEach {
- let c = SoftChip(text: $0)
- chipStack.addArrangedSubview(c)
- }
- daysRibbon.text = daysText
- }
- }
- // MARK: - RecordCard
- final class RecordCard: UIView {
- private let container = UIImageView()
- private let iconView = UIImageView()
- private let titleLabel = UILabel()
- private let dateLabel = UILabel()
- private let badge = StrokeBadge()
- private let bubble = UIView()
- private let summaryLabel = UILabel()
- override init(frame: CGRect) {
- super.init(frame: frame)
- translatesAutoresizingMaskIntoConstraints = false
- container.translatesAutoresizingMaskIntoConstraints = false
- container.image = UIImage(named: "Home378")
- container.contentMode = .scaleToFill
- container.layer.shadowColor = UIColor.black.cgColor
- container.layer.shadowOpacity = 0.06
- container.layer.shadowRadius = 6
- container.layer.shadowOffset = .init(width: 0, height: 2)
- addSubview(container)
- iconView.contentMode = .scaleAspectFill
- iconView.layer.cornerRadius = 0
- iconView.image = UIImage(named: "peihead")
- iconView.layer.masksToBounds = true
- iconView.backgroundColor = UIColor(hex: "#FFF3D9")
- titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
- titleLabel.textColor = UIColor(hex: "#2B2B2B")
- dateLabel.font = .systemFont(ofSize: 12)
- dateLabel.textColor = UIColor(hex: "#8B8B8B")
- bubble.backgroundColor = .white
- bubble.layer.cornerRadius = 12
- summaryLabel.font = .systemFont(ofSize: 13)
- summaryLabel.textColor = UIColor(hex: "#6B6B6B")
- summaryLabel.numberOfLines = 1
- summaryLabel.lineBreakMode = .byTruncatingTail
- [iconView, titleLabel, dateLabel, badge, bubble].forEach {
- $0.translatesAutoresizingMaskIntoConstraints = false
- container.addSubview($0)
- }
- bubble.addSubview(summaryLabel)
- summaryLabel.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- container.topAnchor.constraint(equalTo: topAnchor),
- container.leadingAnchor.constraint(equalTo: leadingAnchor),
- container.trailingAnchor.constraint(equalTo: trailingAnchor),
- container.bottomAnchor.constraint(equalTo: bottomAnchor),
- iconView.topAnchor.constraint(equalTo: container.topAnchor, constant: 17),
- iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 17),
- iconView.widthAnchor.constraint(equalToConstant: 56),
- iconView.heightAnchor.constraint(equalToConstant: 56),
- titleLabel.topAnchor.constraint(equalTo: iconView.topAnchor),
- titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10),
- badge.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
- badge.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -17),
- dateLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
- dateLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
- bubble.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 10),
- bubble.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 12),
- bubble.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -12),
- bubble.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -12),
- bubble.heightAnchor.constraint(equalToConstant: 38),
-
- summaryLabel.leadingAnchor.constraint(equalTo: bubble.leadingAnchor, constant: 12),
- summaryLabel.trailingAnchor.constraint(equalTo: bubble.trailingAnchor, constant: -12),
- summaryLabel.centerYAnchor.constraint(equalTo: bubble.centerYAnchor)
- ])
- }
- required init?(coder: NSCoder) { fatalError() }
- func configure(avatar: UIImage, title: String, date: String, badge: String, summary: String) {
- iconView.image = avatar
- titleLabel.text = title
- dateLabel.text = date
- self.badge.setText(badge)
- summaryLabel.text = summary
- }
- }
- // MARK: - BottomTabBar
- final class BottomTabBar: UIView {
- private let bar = UIView()
- private let leftBtn = BottomTabItem(normalImageName: "Home293", selectedImageName: "Home290", title: "事件", isSelected: true)
- private let rightBtn = BottomTabItem(normalImageName: "Home291", selectedImageName: "Home292", title: "记账")
- private let indicator = UIView()
- var onSelect: ((Int) -> Void)?
- override init(frame: CGRect) {
- super.init(frame: frame)
- translatesAutoresizingMaskIntoConstraints = false
- bar.backgroundColor = .white
- bar.layer.shadowColor = UIColor.black.cgColor
- bar.layer.shadowOpacity = 0.06
- bar.layer.shadowRadius = 6
- bar.layer.shadowOffset = .init(width: 0, height: -2)
- addSubview(bar)
- [leftBtn, rightBtn].forEach {
- bar.addSubview($0)
- $0.translatesAutoresizingMaskIntoConstraints = false
- }
- bar.translatesAutoresizingMaskIntoConstraints = false
- leftBtn.addTarget(self, action: #selector(tapLeft), for: .touchUpInside)
- rightBtn.addTarget(self, action: #selector(tapRight), for: .touchUpInside)
- indicator.backgroundColor = .black
- indicator.layer.cornerRadius = 2
- NSLayoutConstraint.activate([
- bar.topAnchor.constraint(equalTo: topAnchor),
- bar.leadingAnchor.constraint(equalTo: leadingAnchor),
- bar.trailingAnchor.constraint(equalTo: trailingAnchor),
- bar.bottomAnchor.constraint(equalTo: bottomAnchor),
- bar.heightAnchor.constraint(equalToConstant: 64),
- leftBtn.leadingAnchor.constraint(equalTo: bar.leadingAnchor, constant: 82),
- leftBtn.topAnchor.constraint(equalTo: bar.topAnchor,constant: 8),
- leftBtn.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8),
- leftBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 80),
- rightBtn.trailingAnchor.constraint(equalTo: bar.trailingAnchor, constant: -82),
- rightBtn.topAnchor.constraint(equalTo: bar.topAnchor,constant: 8),
- rightBtn.bottomAnchor.constraint(equalTo: bar.bottomAnchor, constant: -8),
- rightBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 80),
- ])
- }
- required init?(coder: NSCoder) { fatalError() }
- @objc private func tapLeft() {
- leftBtn.setSelected(true)
- rightBtn.setSelected(false)
- UIView.animate(withDuration: 0.2) {
- self.indicator.center.x = self.leftBtn.center.x
- }
- onSelect?(0)
- }
- @objc private func tapRight() {
- leftBtn.setSelected(false)
- rightBtn.setSelected(true)
- UIView.animate(withDuration: 0.2) {
- self.indicator.center.x = self.rightBtn.center.x
- }
- onSelect?(1)
- }
- }
- final class BottomTabItem: UIControl {
- private let iconView = UIImageView()
- private let label = UILabel()
- private let normalImage: UIImage?
- private let selectedImage: UIImage?
- init(normalImageName: String, selectedImageName: String, title: String, isSelected: Bool = false) {
- self.normalImage = UIImage(named: normalImageName)?.withRenderingMode(.alwaysOriginal)
- self.selectedImage = UIImage(named: selectedImageName)?.withRenderingMode(.alwaysOriginal)
- super.init(frame: .zero)
- iconView.contentMode = .scaleAspectFit
- iconView.image = normalImage
- label.text = title
- label.font = .systemFont(ofSize: 12)
- label.textColor = UIColor(hex: "#6B6B6B")
- let stack = UIStackView(arrangedSubviews: [iconView, label])
- stack.axis = .vertical
- stack.alignment = .center
- stack.spacing = 4
- addSubview(stack)
- stack.translatesAutoresizingMaskIntoConstraints = false
- iconView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- stack.topAnchor.constraint(equalTo: topAnchor),
- stack.leadingAnchor.constraint(equalTo: leadingAnchor),
- stack.trailingAnchor.constraint(equalTo: trailingAnchor),
- stack.bottomAnchor.constraint(equalTo: bottomAnchor),
- iconView.widthAnchor.constraint(equalToConstant: 22),
- iconView.heightAnchor.constraint(equalToConstant: 22)
- ])
- // 让点击落到整个 control
- stack.isUserInteractionEnabled = false
- setSelected(isSelected)
- }
- required init?(coder: NSCoder) { fatalError() }
- func setSelected(_ sel: Bool) {
- iconView.image = sel ? selectedImage : normalImage
- label.textColor = sel ? .black : UIColor(hex: "#6B6B6B")
- }
- }
- // MARK: - Small Components
- final class SoftChip: PaddingLabel {
- init(text: String) {
- super.init(insets: .init(top: 6, left: 12, bottom: 6, right: 12))
- self.text = text
- font = .systemFont(ofSize: 12, weight: .medium)
- textColor = UIColor(hex: "#6B6B6B")
- backgroundColor = .white
- layer.cornerRadius = 14
- layer.masksToBounds = true
- layer.shadowColor = UIColor.black.cgColor
- layer.shadowOpacity = 0.06
- layer.shadowRadius = 4
- layer.shadowOffset = .init(width: 0, height: 1)
- }
- required init?(coder: NSCoder) { fatalError() }
- }
- final class DottedPill: PaddingLabel {
- private let dashLayer = CAShapeLayer()
- private(set) var isOn: Bool = false
- init(text: String) {
- super.init(insets: .init(top: 8, left: 16, bottom: 8, right: 16))
- self.text = text
- font = .systemFont(ofSize: 12)
- textColor = UIColor(hex: "#5B4227")
- backgroundColor = .clear
- layer.cornerRadius = 10
- layer.masksToBounds = true
- dashLayer.strokeColor = UIColor(hex: "#5B4227").cgColor
- dashLayer.fillColor = UIColor.clear.cgColor
- dashLayer.lineDashPattern = [6, 4]
- dashLayer.lineWidth = 1
- layer.addSublayer(dashLayer)
- applySelectionStyle()
- }
- required init?(coder: NSCoder) { fatalError() }
- func setSelected(_ selected: Bool) {
- isOn = selected
- applySelectionStyle()
- }
- private func applySelectionStyle() {
- if isOn {
- backgroundColor = UIColor(hex: "#FFE059")
- textColor = .black
- dashLayer.isHidden = true
- } else {
- backgroundColor = .clear
- textColor = UIColor(hex: "#5B4227")
- dashLayer.isHidden = false
- }
- }
- override func layoutSubviews() {
- super.layoutSubviews()
- dashLayer.frame = bounds
- dashLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 18).cgPath
- }
- }
- final class StrokeBadge: PaddingLabel {
- init() {
- super.init(insets: .init(top: 4, left: 10, bottom: 4, right: 10))
- textColor = UIColor(hex: "#5B4227")
- font = .systemFont(ofSize: 12)
- layer.cornerRadius = 12
- layer.masksToBounds = true
- layer.borderWidth = 1
- layer.borderColor = UIColor(hex: "#5B4227").cgColor
- }
- required init?(coder: NSCoder) { fatalError() }
- func setText(_ t: String) { text = t }
- }
- // MARK: - PaddingLabel (with insets)
- class PaddingLabel: UILabel {
- private var insets: UIEdgeInsets
- init(insets: UIEdgeInsets) {
- self.insets = insets
- super.init(frame: .zero)
- }
- required init?(coder: NSCoder) { fatalError() }
- override func drawText(in rect: CGRect) {
- super.drawText(in: rect.inset(by: insets))
- }
- override var intrinsicContentSize: CGSize {
- let size = super.intrinsicContentSize
- return CGSize(width: size.width + insets.left + insets.right,
- height: size.height + insets.top + insets.bottom)
- }
- }
|