BookkeepingQuickSheet.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. //
  2. // BookkeepingQuickSheet.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/26.
  6. //
  7. import Foundation
  8. import UIKit
  9. // MARK: - BookkeepingQuickSheet: 快速记账分类弹窗
  10. final class BookkeepingQuickSheet: UIView {
  11. // 外部回调
  12. var onDismiss: (() -> Void)?
  13. var onItemSelected: ((String) -> Void)?
  14. // UI
  15. private let overlay = UIControl()
  16. private let sheet = UIView()
  17. private let header = UIView()
  18. private let backBtn = UIButton(type: .system)
  19. private let titleLabel = UILabel()
  20. private let closeBtn = UIButton(type: .system)
  21. private let scroll = UIScrollView()
  22. private let content = UIStackView()
  23. override init(frame: CGRect) {
  24. super.init(frame: frame)
  25. buildUI()
  26. isHidden = true
  27. }
  28. required init?(coder: NSCoder) { fatalError() }
  29. // 显示/隐藏
  30. func show() {
  31. isHidden = false
  32. overlay.alpha = 0
  33. sheet.alpha = 0
  34. sheet.transform = CGAffineTransform(translationX: 0, y: 24)
  35. layoutIfNeeded()
  36. UIView.animate(withDuration: 0.22) {
  37. self.overlay.alpha = 1
  38. self.sheet.alpha = 1
  39. self.sheet.transform = .identity
  40. }
  41. }
  42. @objc private func hide() {
  43. UIView.animate(withDuration: 0.2, animations: {
  44. self.overlay.alpha = 0
  45. self.sheet.alpha = 0
  46. self.sheet.transform = CGAffineTransform(translationX: 0, y: 10)
  47. }, completion: { _ in
  48. self.isHidden = true
  49. self.onDismiss?()
  50. })
  51. }
  52. private func buildUI() {
  53. backgroundColor = .clear
  54. // 遮罩
  55. overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  56. overlay.alpha = 0
  57. overlay.addTarget(self, action: #selector(hide), for: .touchUpInside)
  58. addSubview(overlay)
  59. overlay.translatesAutoresizingMaskIntoConstraints = false
  60. // 面板
  61. sheet.backgroundColor = .white
  62. sheet.layer.cornerRadius = 16
  63. sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
  64. sheet.layer.masksToBounds = true
  65. addSubview(sheet)
  66. sheet.translatesAutoresizingMaskIntoConstraints = false
  67. // 头部
  68. header.translatesAutoresizingMaskIntoConstraints = false
  69. sheet.addSubview(header)
  70. backBtn.setImage(UIImage(named: "AddPet385")?.withRenderingMode(.alwaysOriginal), for: .normal)
  71. backBtn.addTarget(self, action: #selector(hide), for: .touchUpInside)
  72. titleLabel.text = "记账"
  73. titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
  74. titleLabel.textColor = UIColor(hex: "#2B2B2B")
  75. titleLabel.textAlignment = .center
  76. closeBtn.setImage(UIImage(named: "AddPet384")?.withRenderingMode(.alwaysOriginal), for: .normal)
  77. closeBtn.addTarget(self, action: #selector(hide), for: .touchUpInside)
  78. [backBtn, titleLabel, closeBtn].forEach { v in
  79. v.translatesAutoresizingMaskIntoConstraints = false
  80. header.addSubview(v)
  81. }
  82. // 列表(注意使用 contentLayoutGuide / frameLayoutGuide,避免滚动布局冲突)
  83. scroll.showsVerticalScrollIndicator = false
  84. scroll.translatesAutoresizingMaskIntoConstraints = false
  85. sheet.addSubview(scroll)
  86. content.axis = .vertical
  87. content.spacing = 16
  88. content.translatesAutoresizingMaskIntoConstraints = false
  89. scroll.addSubview(content)
  90. NSLayoutConstraint.activate([
  91. overlay.topAnchor.constraint(equalTo: topAnchor),
  92. overlay.leadingAnchor.constraint(equalTo: leadingAnchor),
  93. overlay.trailingAnchor.constraint(equalTo: trailingAnchor),
  94. overlay.bottomAnchor.constraint(equalTo: bottomAnchor),
  95. sheet.leadingAnchor.constraint(equalTo: leadingAnchor),
  96. sheet.trailingAnchor.constraint(equalTo: trailingAnchor),
  97. sheet.bottomAnchor.constraint(equalTo: bottomAnchor),
  98. sheet.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.65),
  99. header.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8),
  100. header.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  101. header.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  102. header.heightAnchor.constraint(equalToConstant: 44),
  103. backBtn.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
  104. backBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  105. backBtn.widthAnchor.constraint(equalToConstant: 30),
  106. backBtn.heightAnchor.constraint(equalToConstant: 30),
  107. closeBtn.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -12),
  108. closeBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  109. closeBtn.widthAnchor.constraint(equalToConstant: 30),
  110. closeBtn.heightAnchor.constraint(equalToConstant: 30),
  111. titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
  112. titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  113. scroll.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
  114. scroll.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  115. scroll.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  116. scroll.bottomAnchor.constraint(equalTo: sheet.bottomAnchor, constant: -8),
  117. content.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 16),
  118. content.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -16),
  119. content.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor, constant: 8),
  120. content.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor, constant: -16),
  121. content.widthAnchor.constraint(equalTo: scroll.frameLayoutGuide.widthAnchor, constant: -32)
  122. ])
  123. // 数据(分组 + 每行 5 个)
  124. let sections: [(String, [String])] = [
  125. ("食物", ["干粮","罐头","零食","冻干","草粮"]),
  126. ("生活", ["衣服","玩具","餐具","厕所","牵引绳"]),
  127. ("治病", ["驱虫药","保健品","手术","药品","疫苗"]),
  128. ("清洁", ["指甲剪","梳子","清洁用品","洗澡美容","洗护用品"])
  129. ]
  130. for (t, items) in sections {
  131. content.addArrangedSubview(makeSection(title: t, items: items))
  132. }
  133. }
  134. // 标题行:小黄块 + 文本(近似原型的装饰)
  135. private func makeDecorTitle(_ text: String) -> UIView {
  136. let h = UIStackView()
  137. h.axis = .horizontal
  138. h.alignment = .center
  139. h.spacing = 6
  140. let flag = UIView()
  141. flag.backgroundColor = UIColor(hex: "#FFE059")
  142. flag.layer.cornerRadius = 4
  143. flag.translatesAutoresizingMaskIntoConstraints = false
  144. NSLayoutConstraint.activate([
  145. flag.widthAnchor.constraint(equalToConstant: 2),
  146. flag.heightAnchor.constraint(equalToConstant: 20)
  147. ])
  148. let lb = UILabel()
  149. lb.text = text
  150. lb.font = .systemFont(ofSize: 13, weight: .semibold)
  151. lb.textColor = UIColor(hex: "#2B2B2B")
  152. h.addArrangedSubview(flag)
  153. h.addArrangedSubview(lb)
  154. return h
  155. }
  156. private func makeSection(title: String, items: [String]) -> UIView {
  157. let wrap = UIStackView()
  158. wrap.axis = .vertical
  159. wrap.spacing = 8
  160. let titleView = makeDecorTitle(title)
  161. wrap.addArrangedSubview(titleView)
  162. let grid = UIStackView()
  163. grid.axis = .vertical
  164. grid.spacing = 12
  165. wrap.addArrangedSubview(grid)
  166. var row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
  167. grid.addArrangedSubview(row)
  168. var count = 0
  169. for name in items {
  170. if count == 5 {
  171. row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
  172. grid.addArrangedSubview(row)
  173. count = 0
  174. }
  175. let v = makeIconChip(title: name)
  176. row.addArrangedSubview(v)
  177. count += 1
  178. }
  179. return wrap
  180. }
  181. // 方形图标(56x56)+ 文本(上图下字),与原型一致
  182. private func makeIconChip(title: String) -> UIView {
  183. let holder = UIView()
  184. let container = UIStackView()
  185. container.axis = .vertical
  186. container.alignment = .center
  187. container.spacing = 6
  188. holder.addSubview(container)
  189. container.translatesAutoresizingMaskIntoConstraints = false
  190. NSLayoutConstraint.activate([
  191. container.topAnchor.constraint(equalTo: holder.topAnchor),
  192. container.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
  193. container.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
  194. container.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
  195. ])
  196. let bg = UIView()
  197. bg.backgroundColor = .white
  198. bg.layer.cornerRadius = 12
  199. bg.layer.borderWidth = 1
  200. bg.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor
  201. bg.translatesAutoresizingMaskIntoConstraints = false
  202. let iv = UIImageView(image: UIImage(named: "peihead"))
  203. iv.contentMode = .scaleAspectFit
  204. iv.translatesAutoresizingMaskIntoConstraints = false
  205. bg.addSubview(iv)
  206. NSLayoutConstraint.activate([
  207. bg.widthAnchor.constraint(equalToConstant: 56),
  208. bg.heightAnchor.constraint(equalToConstant: 56),
  209. iv.centerXAnchor.constraint(equalTo: bg.centerXAnchor),
  210. iv.centerYAnchor.constraint(equalTo: bg.centerYAnchor),
  211. iv.widthAnchor.constraint(equalToConstant: 40),
  212. iv.heightAnchor.constraint(equalToConstant: 40)
  213. ])
  214. let label = UILabel()
  215. label.text = title
  216. label.font = .systemFont(ofSize: 12)
  217. label.textColor = UIColor(hex: "#6B6B6B")
  218. container.addArrangedSubview(bg)
  219. container.addArrangedSubview(label)
  220. // 全覆盖按钮用于点击
  221. let tap = UIButton(type: .system)
  222. tap.backgroundColor = .clear
  223. tap.accessibilityLabel = title
  224. tap.addTarget(self, action: #selector(onItemTap(_:)), for: .touchUpInside)
  225. holder.addSubview(tap)
  226. tap.translatesAutoresizingMaskIntoConstraints = false
  227. NSLayoutConstraint.activate([
  228. tap.topAnchor.constraint(equalTo: holder.topAnchor),
  229. tap.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
  230. tap.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
  231. tap.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
  232. ])
  233. return holder
  234. }
  235. @objc private func onItemTap(_ sender: UIButton) {
  236. let name = sender.accessibilityLabel ?? ""
  237. onItemSelected?(name)
  238. // hide()
  239. }
  240. }