| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- //
- // 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
- }
- }
|