| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- //
- // BookkeepingQuickSheet.swift
- // VenusKitto
- //
- // Created by Neoa on 2025/8/26.
- //
- import Foundation
- import UIKit
- // MARK: - BookkeepingQuickSheet: 快速记账分类弹窗
- final class BookkeepingQuickSheet: UIView {
- // 外部回调
- var onDismiss: (() -> Void)?
- var onItemSelected: ((String) -> Void)?
- // UI
- private let overlay = UIControl()
- private let sheet = UIView()
- private let header = UIView()
- private let backBtn = UIButton(type: .system)
- private let titleLabel = UILabel()
- private let closeBtn = UIButton(type: .system)
- private let scroll = UIScrollView()
- private let content = UIStackView()
- override init(frame: CGRect) {
- super.init(frame: frame)
- buildUI()
- isHidden = true
- }
- required init?(coder: NSCoder) { fatalError() }
- // 显示/隐藏
- 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() {
- UIView.animate(withDuration: 0.2, animations: {
- self.overlay.alpha = 0
- self.sheet.alpha = 0
- self.sheet.transform = CGAffineTransform(translationX: 0, y: 10)
- }, completion: { _ in
- self.isHidden = true
- self.onDismiss?()
- })
- }
- private func buildUI() {
- backgroundColor = .clear
- // 遮罩
- overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
- overlay.alpha = 0
- 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
- // 头部
- header.translatesAutoresizingMaskIntoConstraints = false
- sheet.addSubview(header)
- backBtn.setImage(UIImage(named: "AddPet385")?.withRenderingMode(.alwaysOriginal), for: .normal)
- backBtn.addTarget(self, action: #selector(hide), for: .touchUpInside)
- titleLabel.text = "记账"
- titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
- titleLabel.textColor = UIColor(hex: "#2B2B2B")
- titleLabel.textAlignment = .center
- closeBtn.setImage(UIImage(named: "AddPet384")?.withRenderingMode(.alwaysOriginal), for: .normal)
- closeBtn.addTarget(self, action: #selector(hide), for: .touchUpInside)
- [backBtn, titleLabel, closeBtn].forEach { v in
- v.translatesAutoresizingMaskIntoConstraints = false
- header.addSubview(v)
- }
- // 列表(注意使用 contentLayoutGuide / frameLayoutGuide,避免滚动布局冲突)
- scroll.showsVerticalScrollIndicator = false
- scroll.translatesAutoresizingMaskIntoConstraints = false
- sheet.addSubview(scroll)
- content.axis = .vertical
- content.spacing = 16
- content.translatesAutoresizingMaskIntoConstraints = false
- scroll.addSubview(content)
- 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: 0.65),
- header.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8),
- header.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
- header.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
- header.heightAnchor.constraint(equalToConstant: 44),
- backBtn.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
- backBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
- backBtn.widthAnchor.constraint(equalToConstant: 30),
- backBtn.heightAnchor.constraint(equalToConstant: 30),
- closeBtn.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -12),
- closeBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
- closeBtn.widthAnchor.constraint(equalToConstant: 30),
- closeBtn.heightAnchor.constraint(equalToConstant: 30),
- titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
- titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
- scroll.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
- scroll.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
- scroll.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
- scroll.bottomAnchor.constraint(equalTo: sheet.bottomAnchor, constant: -8),
- content.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 16),
- content.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -16),
- content.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor, constant: 8),
- content.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor, constant: -16),
- content.widthAnchor.constraint(equalTo: scroll.frameLayoutGuide.widthAnchor, constant: -32)
- ])
- // 数据(分组 + 每行 5 个)
- let sections: [(String, [String])] = [
- ("食物", ["干粮","罐头","零食","冻干","草粮"]),
- ("生活", ["衣服","玩具","餐具","厕所","牵引绳"]),
- ("治病", ["驱虫药","保健品","手术","药品","疫苗"]),
- ("清洁", ["指甲剪","梳子","清洁用品","洗澡美容","洗护用品"])
- ]
- for (t, items) in sections {
- content.addArrangedSubview(makeSection(title: t, items: items))
- }
- }
- // 标题行:小黄块 + 文本(近似原型的装饰)
- private func makeDecorTitle(_ text: String) -> UIView {
- let h = UIStackView()
- h.axis = .horizontal
- h.alignment = .center
- h.spacing = 6
- let flag = UIView()
- flag.backgroundColor = UIColor(hex: "#FFE059")
- flag.layer.cornerRadius = 4
- flag.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- flag.widthAnchor.constraint(equalToConstant: 2),
- flag.heightAnchor.constraint(equalToConstant: 20)
- ])
- let lb = UILabel()
- lb.text = text
- lb.font = .systemFont(ofSize: 13, weight: .semibold)
- lb.textColor = UIColor(hex: "#2B2B2B")
- h.addArrangedSubview(flag)
- h.addArrangedSubview(lb)
- return h
- }
- private func makeSection(title: String, items: [String]) -> UIView {
- let wrap = UIStackView()
- wrap.axis = .vertical
- wrap.spacing = 8
- let titleView = makeDecorTitle(title)
- wrap.addArrangedSubview(titleView)
- let grid = UIStackView()
- grid.axis = .vertical
- grid.spacing = 12
- wrap.addArrangedSubview(grid)
- var row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
- grid.addArrangedSubview(row)
- var count = 0
- for name in items {
- if count == 5 {
- row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
- grid.addArrangedSubview(row)
- count = 0
- }
- let v = makeIconChip(title: name)
- row.addArrangedSubview(v)
- count += 1
- }
- return wrap
- }
- // 方形图标(56x56)+ 文本(上图下字),与原型一致
- private func makeIconChip(title: String) -> UIView {
- let holder = UIView()
- let container = UIStackView()
- container.axis = .vertical
- container.alignment = .center
- container.spacing = 6
- holder.addSubview(container)
- container.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- container.topAnchor.constraint(equalTo: holder.topAnchor),
- container.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
- container.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
- container.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
- ])
- let bg = UIView()
- bg.backgroundColor = .white
- bg.layer.cornerRadius = 12
- bg.layer.borderWidth = 1
- bg.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor
- bg.translatesAutoresizingMaskIntoConstraints = false
- let iv = UIImageView(image: UIImage(named: "peihead"))
- iv.contentMode = .scaleAspectFit
- iv.translatesAutoresizingMaskIntoConstraints = false
- bg.addSubview(iv)
- NSLayoutConstraint.activate([
- bg.widthAnchor.constraint(equalToConstant: 56),
- bg.heightAnchor.constraint(equalToConstant: 56),
- iv.centerXAnchor.constraint(equalTo: bg.centerXAnchor),
- iv.centerYAnchor.constraint(equalTo: bg.centerYAnchor),
- iv.widthAnchor.constraint(equalToConstant: 40),
- iv.heightAnchor.constraint(equalToConstant: 40)
- ])
- let label = UILabel()
- label.text = title
- label.font = .systemFont(ofSize: 12)
- label.textColor = UIColor(hex: "#6B6B6B")
- container.addArrangedSubview(bg)
- container.addArrangedSubview(label)
- // 全覆盖按钮用于点击
- let tap = UIButton(type: .system)
- tap.backgroundColor = .clear
- tap.accessibilityLabel = title
- tap.addTarget(self, action: #selector(onItemTap(_:)), for: .touchUpInside)
- holder.addSubview(tap)
- tap.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- tap.topAnchor.constraint(equalTo: holder.topAnchor),
- tap.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
- tap.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
- tap.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
- ])
- return holder
- }
- @objc private func onItemTap(_ sender: UIButton) {
- let name = sender.accessibilityLabel ?? ""
- onItemSelected?(name)
- // hide()
- }
- }
|