// // BookkeepingAmountSheet.swift // VenusKitto // // Created by Neoa on 2025/8/26. // import Foundation import UIKit // MARK: - BookkeepingAmountSheet: 记账输入弹窗(上图下字 + 数字键盘) final class BookkeepingAmountSheet: UIView{ // 外部交互 var onDismiss: (() -> Void)? /// (分类名, 日期, 选中宠物, 备注, 金额字符串[带正负/小数]) var onSave: ((String, Date, HomeViewController.PetSummary?, String, String) -> Void)? /// 让宿主控制选择日期(可用你项目里的 DatePickerSheetController) var onPickDate: ((_ current: Date, _ handler: @escaping (Date) -> Void) -> Void)? /// 让宿主控制选择宠物(ActionSheet/自定义弹窗都行) var onPickPet: ((_ handler: @escaping (HomeViewController.PetSummary) -> Void) -> Void)? // 内部状态 private var categoryName: String = "" private var selectedDate: Date = Date() private var selectedPet: HomeViewController.PetSummary? private var amountText: String = "" { didSet { amountField.text = amountText } } // UI private let overlay = UIControl() private let sheet = UIView() private let handleBar = UIView() private let dateButton = UIButton(type: .system) private let petButton = UIControl() private let petAvatar = UIImageView() private let petNameLabel = UILabel() private let noteField = UITextField() private let amountField = UITextField() private let grid = UIStackView() // MARK: - LifeCycle override init(frame: CGRect) { super.init(frame: frame) buildUI() isHidden = true } required init?(coder: NSCoder) { fatalError() } // 对外:配置分类、当前宠物 func configure(category: String, currentPet: HomeViewController.PetSummary?) { self.categoryName = category self.selectedPet = currentPet petNameLabel.text = currentPet?.name ?? "宠物名字" if let urlStr = currentPet?.avatar, let u = URL(string: urlStr) { // 简单加载头像(可替换为你的统一加载逻辑) URLSession.shared.dataTask(with: u) { data, _, _ in let img = data.flatMap { UIImage(data: $0) } DispatchQueue.main.async { self.petAvatar.image = img ?? UIImage(named: "peihead") } }.resume() } else { petAvatar.image = UIImage(named: "peihead") } dateButton.setTitle(format(selectedDate), for: .normal) noteField.text = "" amountText = "" } // 显示/隐藏 func show() { isHidden = false overlay.alpha = 0 sheet.alpha = 0 sheet.transform = CGAffineTransform(translationX: 0, y: 24) layoutIfNeeded() UIView.animate(withDuration: 0.22) { self.overlay.alpha = 1 self.sheet.alpha = 1 self.sheet.transform = .identity } } @objc private func hide() { endEditing(true) // 收起键盘 UIView.animate(withDuration: 0.2, animations: { self.overlay.alpha = 0 self.sheet.alpha = 0 self.sheet.transform = CGAffineTransform(translationX: 0, y: 16) }, completion: { _ in self.isHidden = true self.onDismiss?() }) } // MARK: - UI private func buildUI() { backgroundColor = .clear // 遮罩 overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5) overlay.addTarget(self, action: #selector(hide), for: .touchUpInside) addSubview(overlay) overlay.translatesAutoresizingMaskIntoConstraints = false // 底板 sheet.backgroundColor = .white sheet.layer.cornerRadius = 16 sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] sheet.layer.masksToBounds = true addSubview(sheet) sheet.translatesAutoresizingMaskIntoConstraints = false // 顶部小横条 handleBar.backgroundColor = UIColor(hex: "#E9E5E1") handleBar.layer.cornerRadius = 2 handleBar.translatesAutoresizingMaskIntoConstraints = false sheet.addSubview(handleBar) // 顶部:日期 & 宠物 let topRow = UIStackView() topRow.axis = .horizontal topRow.spacing = 12 topRow.distribution = .fillEqually topRow.translatesAutoresizingMaskIntoConstraints = false sheet.addSubview(topRow) // 日期按钮(黄底,左侧) dateButton.setTitleColor(.black, for: .normal) dateButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .semibold) dateButton.backgroundColor = UIColor(hex: "#FFE059") dateButton.layer.cornerRadius = 10 dateButton.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12) dateButton.heightAnchor.constraint(equalToConstant: 44).isActive = true dateButton.addTarget(self, action: #selector(tapDate), for: .touchUpInside) // 宠物选择(白底描边,右侧) let petContainer = UIView() petContainer.backgroundColor = UIColor(hex: "#FEFCF2") petContainer.layer.cornerRadius = 10 petContainer.layer.borderWidth = 1 petContainer.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor petContainer.translatesAutoresizingMaskIntoConstraints = false petButton.translatesAutoresizingMaskIntoConstraints = false petContainer.addSubview(petButton) let petStack = UIStackView() petStack.axis = .horizontal petStack.alignment = .center petStack.spacing = 8 petStack.translatesAutoresizingMaskIntoConstraints = false petContainer.addSubview(petStack) petAvatar.contentMode = .scaleAspectFill petAvatar.layer.cornerRadius = 14 petAvatar.layer.masksToBounds = true petAvatar.widthAnchor.constraint(equalToConstant: 28).isActive = true petAvatar.heightAnchor.constraint(equalToConstant: 28).isActive = true petNameLabel.text = "宠物名字" petNameLabel.font = .systemFont(ofSize: 15) petStack.addArrangedSubview(petAvatar) petStack.addArrangedSubview(petNameLabel) NSLayoutConstraint.activate([ petButton.topAnchor.constraint(equalTo: petContainer.topAnchor), petButton.bottomAnchor.constraint(equalTo: petContainer.bottomAnchor), petButton.leadingAnchor.constraint(equalTo: petContainer.leadingAnchor), petButton.trailingAnchor.constraint(equalTo: petContainer.trailingAnchor), petStack.centerYAnchor.constraint(equalTo: petContainer.centerYAnchor), petStack.leadingAnchor.constraint(equalTo: petContainer.leadingAnchor, constant: 12), petStack.trailingAnchor.constraint(lessThanOrEqualTo: petContainer.trailingAnchor, constant: -12), petContainer.heightAnchor.constraint(equalToConstant: 44) ]) petButton.addTarget(self, action: #selector(tapPet), for: .touchUpInside) topRow.addArrangedSubview(dateButton) topRow.addArrangedSubview(petContainer) // 第二行:备注/金额 let midRow = UIStackView() midRow.axis = .horizontal midRow.spacing = 12 midRow.distribution = .fillEqually midRow.translatesAutoresizingMaskIntoConstraints = false sheet.addSubview(midRow) styleField(noteField, placeholder: "备注") noteField.returnKeyType = .done noteField.delegate = self noteField.inputAccessoryView = makeKeyboardToolbar() // 键盘上方“完成”按钮 styleField(amountField, placeholder: "请输入金额") amountField.textAlignment = .center amountField.isUserInteractionEnabled = false // 交给数字键盘输入 midRow.addArrangedSubview(noteField) midRow.addArrangedSubview(amountField) // 数字键盘 4×4 grid.axis = .vertical grid.spacing = 12 grid.translatesAutoresizingMaskIntoConstraints = false sheet.addSubview(grid) let rows: [[String]] = [ ["1","2","3","C"], ["4","5","6","+"], ["7","8","9","-"], ["再记","0",".","保存"] ] for r in rows { let h = UIStackView() h.axis = .horizontal h.spacing = 12 h.distribution = .fillEqually r.forEach { h.addArrangedSubview(makeKey($0)) } grid.addArrangedSubview(h) } // 点击任意空白处收起键盘(不影响其它控件点击) let tapDismiss = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tapDismiss.cancelsTouchesInView = false sheet.addGestureRecognizer(tapDismiss) // 约束 NSLayoutConstraint.activate([ overlay.topAnchor.constraint(equalTo: topAnchor), overlay.leadingAnchor.constraint(equalTo: leadingAnchor), overlay.trailingAnchor.constraint(equalTo: trailingAnchor), overlay.bottomAnchor.constraint(equalTo: bottomAnchor), sheet.leadingAnchor.constraint(equalTo: leadingAnchor), sheet.trailingAnchor.constraint(equalTo: trailingAnchor), sheet.bottomAnchor.constraint(equalTo: bottomAnchor), // sheet.heightAnchor.constraint(equalTo: heightAnchor, multiplier: (traitCollection.userInterfaceIdiom == .pad ? 0.30 : 0.44)), sheet.heightAnchor.constraint(equalToConstant: 421), handleBar.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8), handleBar.centerXAnchor.constraint(equalTo: sheet.centerXAnchor), handleBar.widthAnchor.constraint(equalToConstant: 44), handleBar.heightAnchor.constraint(equalToConstant: 4), topRow.topAnchor.constraint(equalTo: handleBar.bottomAnchor, constant: 16), topRow.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 16), topRow.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -16), midRow.topAnchor.constraint(equalTo: topRow.bottomAnchor, constant: 12), midRow.leadingAnchor.constraint(equalTo: topRow.leadingAnchor), midRow.trailingAnchor.constraint(equalTo: topRow.trailingAnchor), noteField.heightAnchor.constraint(equalToConstant: 44), amountField.heightAnchor.constraint(equalTo: noteField.heightAnchor), grid.topAnchor.constraint(equalTo: midRow.bottomAnchor, constant: 16), grid.leadingAnchor.constraint(equalTo: topRow.leadingAnchor), grid.trailingAnchor.constraint(equalTo: topRow.trailingAnchor), grid.bottomAnchor.constraint(equalTo: sheet.safeAreaLayoutGuide.bottomAnchor, constant: -12) ]) } private func styleField(_ tf: UITextField, placeholder: String) { tf.placeholder = placeholder tf.font = .systemFont(ofSize: 15) tf.backgroundColor = UIColor(hex: "#FEFCF2") tf.layer.cornerRadius = 10 tf.layer.borderWidth = 1 tf.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor tf.leftView = UIView(frame: .init(x: 0, y: 0, width: 12, height: 1)) tf.leftViewMode = .always tf.heightAnchor.constraint(equalToConstant: 44).isActive = true } private func makeKey(_ title: String) -> UIView { let btn = UIButton(type: .system) btn.setTitle(title, for: .normal) btn.titleLabel?.font = .systemFont(ofSize: 18, weight: (title == "保存" ? .semibold : .regular)) btn.layer.cornerRadius = 10 btn.heightAnchor.constraint(equalToConstant: 48).isActive = true if title == "保存" || title == "再记" { btn.backgroundColor = (title == "保存") ? UIColor(hex: "#FFE059") : UIColor(hex: "#FFE059") btn.setTitleColor(.black, for: .normal) } else { btn.backgroundColor = UIColor(hex: "#FEFCF2") btn.setTitleColor(UIColor(hex: "#2B2B2B"), for: .normal) btn.layer.borderWidth = 1 btn.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor } btn.addTarget(self, action: #selector(tapKey(_:)), for: .touchUpInside) return btn } @objc private func dismissKeyboard() { endEditing(true) } private func makeKeyboardToolbar() -> UIToolbar { let bar = UIToolbar() bar.sizeToFit() let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) let done = UIBarButtonItem(title: "完成", style: .done, target: self, action: #selector(dismissKeyboard)) bar.items = [flex, done] return bar } // MARK: - Actions @objc private func tapDate() { onPickDate?(selectedDate) { [weak self] newDate in guard let self = self else { return } self.selectedDate = newDate self.dateButton.setTitle(self.format(newDate), for: .normal) } } @objc private func tapPet() { onPickPet? { [weak self] pet in guard let self = self else { return } self.selectedPet = pet self.petNameLabel.text = pet.name if let avatar = pet.avatar, let url = URL(string: avatar) { URLSession.shared.dataTask(with: url) { data, _, _ in let img = data.flatMap { UIImage(data: $0) } DispatchQueue.main.async { self.petAvatar.image = img ?? UIImage(named: "peihead") } }.resume() } } } @objc private func tapKey(_ sender: UIButton) { guard let t = sender.title(for: .normal) else { return } switch t { case "0"..."9": amountText.append(t) case ".": if !amountText.contains(".") { amountText.append(".") } case "C": if !amountText.isEmpty { amountText.removeLast() } case "+": if amountText.hasPrefix("-") { amountText.removeFirst() } case "-": if !amountText.hasPrefix("-"), !amountText.isEmpty { amountText = "-" + amountText } case "再记": onSave?(categoryName, selectedDate, selectedPet, noteField.text ?? "", amountText) noteField.text = "" amountText = "" case "保存": onSave?(categoryName, selectedDate, selectedPet, noteField.text ?? "", amountText) hide() default: break } } private func format(_ d: Date) -> String { let f = DateFormatter() f.locale = Locale(identifier: "zh_CN") f.dateFormat = "yyyy.MM.dd" return f.string(from: d) } } extension BookkeepingAmountSheet: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }