CreateRecordSheet.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. //
  2. // CreateRecordSheet.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/25.
  6. //
  7. import Foundation
  8. import UIKit
  9. /// 自定义创建记录弹窗(含半透明遮罩 + 白色面板)
  10. final class CreateRecordSheet: UIView, UITextViewDelegate {
  11. // 需要添加一个viewController引用
  12. weak var viewController: UIViewController?
  13. // MARK: - UI
  14. private let overlay = UIControl()
  15. private let sheet = UIView()
  16. private let header = UIView()
  17. private let backBtn = UIButton(type: .system)
  18. private let titleLabel = UILabel()
  19. private let closeBtn = UIButton(type: .system)
  20. private let memoBtn = UIButton(type: .system)
  21. private let dash = CAShapeLayer()
  22. private let scroll = UIScrollView()
  23. private let content = UIStackView()
  24. // Editor (Quick Memo)
  25. private var editorView: UIView?
  26. private var editorBuilt = false
  27. private let editorTextView = UITextView()
  28. private let editorPlaceholder = UILabel()
  29. // Editor title & tag
  30. private let editorTitleLabel = UILabel()
  31. private let tagButton = UIButton(type: .system)
  32. private var selectedEventName: String = "随手小记"
  33. // 事件标题到 recordTypeId 的映射(按后端给定)
  34. private let recordTypeMap: [String: Int] = [
  35. "吃饭": 1, "喝水": 2, "喂奶": 3, "尿便": 4, "铲屎": 5,
  36. "驱虫": 6, "喂药": 7, "疫苗": 8, "看病": 9, "手术": 10,
  37. "洗澡": 11, "梳毛": 12, "剪指甲": 13, "洗耳朵": 14,
  38. // 后端示例为“挤肛门踢”,也兼容“挤肛门腺”
  39. "挤肛门踢": 15, "挤肛门腺": 15,
  40. "洗笼子": 16, "洗食盆": 17, "洗水盆": 18, "洗玩具": 19, "换耗材": 20
  41. ]
  42. // 新增:保存选择的petId
  43. private var selectedPetId: Int?
  44. // Tag picker
  45. // 添加TagPickerView属性
  46. private var tagPickerView: TagPickerView!
  47. // 在 CreateRecordSheet 中添加日期选择的功能
  48. private var selectedDate: Date?
  49. // 对外回调(可选)
  50. var onDismiss: (() -> Void)?
  51. // rows 属性定义
  52. private var rows = UIStackView()
  53. private var dateLabel = UILabel()
  54. private var petAvatar = UIImageView()
  55. private var petNameLabel = UILabel()
  56. // MARK: - Life
  57. override init(frame: CGRect) {
  58. super.init(frame: frame)
  59. buildUI()
  60. isHidden = true
  61. }
  62. required init?(coder: NSCoder) { fatalError() }
  63. // MARK: - Public
  64. func show() {
  65. isHidden = false
  66. overlay.alpha = 0
  67. sheet.transform = CGAffineTransform(translationX: 0, y: 30)
  68. sheet.alpha = 0
  69. setNeedsLayout()
  70. layoutIfNeeded()
  71. UIView.animate(withDuration: 0.22) {
  72. self.overlay.alpha = 1
  73. self.sheet.alpha = 1
  74. self.sheet.transform = .identity
  75. }
  76. }
  77. @objc func hide() {
  78. UIView.animate(withDuration: 0.2, animations: {
  79. self.overlay.alpha = 0
  80. self.sheet.alpha = 0
  81. self.sheet.transform = CGAffineTransform(translationX: 0, y: 10)
  82. }, completion: { _ in
  83. self.isHidden = true
  84. self.onDismiss?()
  85. })
  86. }
  87. // MARK: - Build
  88. private func buildUI() {
  89. backgroundColor = .clear
  90. // overlay
  91. overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
  92. overlay.alpha = 0
  93. overlay.addTarget(self, action: #selector(hide), for: .touchUpInside)
  94. addSubview(overlay)
  95. // sheet
  96. sheet.backgroundColor = .white
  97. sheet.layer.cornerRadius = 16
  98. sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
  99. sheet.layer.masksToBounds = true
  100. sheet.alpha = 0
  101. addSubview(sheet)
  102. // header
  103. backBtn.setImage(UIImage(named: "AddPet385")?.withRenderingMode(.alwaysOriginal), for: .normal)
  104. backBtn.tintColor = UIColor(hex: "#2B2B2B")
  105. backBtn.addTarget(self, action: #selector(onBackTapped), for: .touchUpInside)
  106. titleLabel.text = "创建记录"
  107. titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
  108. titleLabel.textColor = UIColor(hex: "#2B2B2B")
  109. titleLabel.textAlignment = .center
  110. closeBtn.setImage(UIImage(named: "AddPet384")?.withRenderingMode(.alwaysOriginal), for: .normal)
  111. closeBtn.tintColor = UIColor(hex: "#2B2B2B")
  112. closeBtn.addTarget(self, action: #selector(hide), for: .touchUpInside)
  113. [header, backBtn, titleLabel, closeBtn].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
  114. sheet.addSubview(header)
  115. header.addSubview(backBtn)
  116. header.addSubview(titleLabel)
  117. header.addSubview(closeBtn)
  118. // dashed memo button
  119. memoBtn.setTitle("随手小记", for: .normal)
  120. memoBtn.setTitleColor(UIColor(hex: "#2B2B2B"), for: .normal)
  121. memoBtn.titleLabel?.font = .systemFont(ofSize: 15)
  122. memoBtn.backgroundColor = UIColor(hex: "#FCFCFC")
  123. memoBtn.layer.cornerRadius = 14
  124. memoBtn.layer.masksToBounds = true
  125. memoBtn.translatesAutoresizingMaskIntoConstraints = false
  126. sheet.addSubview(memoBtn)
  127. dash.strokeColor = UIColor(hex: "#5B4227").cgColor
  128. dash.fillColor = UIColor.clear.cgColor
  129. // 使用 NSNumber,避免某些系统下的桥接问题
  130. dash.lineDashPattern = [NSNumber(value: 6), NSNumber(value: 4)]
  131. dash.lineCap = .round
  132. dash.lineWidth = 1
  133. dash.zPosition = 1
  134. memoBtn.layer.addSublayer(dash)
  135. // scroll + content
  136. scroll.showsVerticalScrollIndicator = false
  137. scroll.translatesAutoresizingMaskIntoConstraints = false
  138. sheet.addSubview(scroll)
  139. content.axis = .vertical
  140. content.spacing = 16
  141. content.translatesAutoresizingMaskIntoConstraints = false
  142. scroll.addSubview(content)
  143. overlay.translatesAutoresizingMaskIntoConstraints = false
  144. sheet.translatesAutoresizingMaskIntoConstraints = false
  145. // 创建TagPickerView
  146. tagPickerView = TagPickerView(frame: CGRect.zero)
  147. tagPickerView.translatesAutoresizingMaskIntoConstraints = false
  148. addSubview(tagPickerView)
  149. tagPickerView.onTagSelected = { [weak self] selectedTag in
  150. guard let self = self else { return }
  151. self.selectedEventName = selectedTag
  152. self.editorTitleLabel.text = selectedTag
  153. }
  154. NSLayoutConstraint.activate([
  155. overlay.topAnchor.constraint(equalTo: topAnchor),
  156. overlay.leadingAnchor.constraint(equalTo: leadingAnchor),
  157. overlay.trailingAnchor.constraint(equalTo: trailingAnchor),
  158. overlay.bottomAnchor.constraint(equalTo: bottomAnchor),
  159. sheet.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0),
  160. sheet.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0),
  161. sheet.bottomAnchor.constraint(equalTo: bottomAnchor),
  162. sheet.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.75),
  163. header.topAnchor.constraint(equalTo: sheet.topAnchor, constant: 8),
  164. header.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  165. header.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  166. header.heightAnchor.constraint(equalToConstant: 44),
  167. backBtn.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
  168. backBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  169. backBtn.widthAnchor.constraint(equalToConstant: 30),
  170. backBtn.heightAnchor.constraint(equalToConstant: 30),
  171. closeBtn.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -12),
  172. closeBtn.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  173. closeBtn.widthAnchor.constraint(equalToConstant: 30),
  174. closeBtn.heightAnchor.constraint(equalToConstant: 30),
  175. titleLabel.centerXAnchor.constraint(equalTo: header.centerXAnchor),
  176. titleLabel.centerYAnchor.constraint(equalTo: header.centerYAnchor),
  177. memoBtn.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 8),
  178. memoBtn.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 16),
  179. memoBtn.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: -16),
  180. memoBtn.heightAnchor.constraint(equalToConstant: 48),
  181. scroll.topAnchor.constraint(equalTo: memoBtn.bottomAnchor, constant: 12),
  182. scroll.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  183. scroll.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  184. scroll.bottomAnchor.constraint(equalTo: sheet.bottomAnchor, constant: -8),
  185. content.topAnchor.constraint(equalTo: scroll.topAnchor, constant: 8),
  186. content.leadingAnchor.constraint(equalTo: scroll.leadingAnchor, constant: 16),
  187. content.trailingAnchor.constraint(equalTo: scroll.trailingAnchor, constant: -16),
  188. content.bottomAnchor.constraint(equalTo: scroll.bottomAnchor, constant: -16),
  189. content.widthAnchor.constraint(equalTo: scroll.widthAnchor, constant: -32),
  190. tagPickerView.topAnchor.constraint(equalTo: sheet.topAnchor),
  191. tagPickerView.leadingAnchor.constraint(equalTo: sheet.leadingAnchor),
  192. tagPickerView.trailingAnchor.constraint(equalTo: sheet.trailingAnchor),
  193. tagPickerView.bottomAnchor.constraint(equalTo: sheet.bottomAnchor)
  194. ])
  195. // 隐藏TagPickerView
  196. tagPickerView.isHidden = true
  197. // Sections
  198. let sections: [(String, [String])] = [
  199. ("日常事项", ["吃饭","喝水","喂奶","尿便","铲屎"]),
  200. ("健康事项", ["驱虫","喂药","疫苗","看病","手术"]),
  201. ("清洁事项", ["洗澡","梳毛","剪指甲","洗耳朵","挤肛门腺"]),
  202. ("打扫事项", ["洗笼子","洗食盆","洗水盆","洗玩具","换耗材"])
  203. ]
  204. for (t, items) in sections { content.addArrangedSubview(makeSection(title: t, items: items)) }
  205. memoBtn.addTarget(self, action: #selector(onMemoTapped), for: .touchUpInside)
  206. // Pre-build editor container (hidden by default)
  207. buildQuickMemoEditorIfNeeded()
  208. }
  209. override func layoutSubviews() {
  210. super.layoutSubviews()
  211. // Recompute dashed border path after Auto Layout has finalized frames
  212. let bounds = memoBtn.bounds
  213. guard bounds.width > 0 && bounds.height > 0 else { return }
  214. dash.contentsScale = UIScreen.main.scale
  215. dash.frame = bounds
  216. let inset: CGFloat = dash.lineWidth / 2
  217. let rect = bounds.insetBy(dx: inset, dy: inset)
  218. let radius = max(0, 14 - inset)
  219. dash.path = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath
  220. }
  221. // MARK: - Helpers
  222. private func makeSection(title: String, items: [String]) -> UIView {
  223. let wrap = UIStackView()
  224. wrap.axis = .vertical
  225. wrap.spacing = 8
  226. let titleLabel = UILabel()
  227. titleLabel.text = title
  228. titleLabel.font = .systemFont(ofSize: 13, weight: .semibold)
  229. titleLabel.textColor = UIColor(hex: "#2B2B2B")
  230. wrap.addArrangedSubview(titleLabel)
  231. let grid = UIStackView()
  232. grid.axis = .vertical
  233. grid.spacing = 12
  234. wrap.addArrangedSubview(grid)
  235. var row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
  236. grid.addArrangedSubview(row)
  237. var count = 0
  238. for name in items {
  239. if count == 5 {
  240. row = UIStackView(); row.axis = .horizontal; row.spacing = 16; row.distribution = .fillEqually
  241. grid.addArrangedSubview(row)
  242. count = 0
  243. }
  244. let v = makeIconChip(title: name, imageName: "peihead")
  245. row.addArrangedSubview(v)
  246. count += 1
  247. }
  248. return wrap
  249. }
  250. private func makeIconChip(title: String, imageName: String) -> UIView {
  251. let holder = UIView()
  252. let container = UIStackView()
  253. container.axis = .vertical
  254. container.alignment = .center
  255. container.spacing = 6
  256. holder.addSubview(container)
  257. container.translatesAutoresizingMaskIntoConstraints = false
  258. NSLayoutConstraint.activate([
  259. container.topAnchor.constraint(equalTo: holder.topAnchor),
  260. container.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
  261. container.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
  262. container.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
  263. ])
  264. let bg = UIView()
  265. bg.backgroundColor = .white
  266. bg.layer.cornerRadius = 12
  267. bg.layer.borderWidth = 1
  268. bg.layer.borderColor = UIColor(hex: "#E9E5E1").cgColor
  269. bg.translatesAutoresizingMaskIntoConstraints = false
  270. let iv = UIImageView(image: UIImage(named: imageName))
  271. iv.contentMode = .scaleAspectFit
  272. iv.translatesAutoresizingMaskIntoConstraints = false
  273. bg.addSubview(iv)
  274. NSLayoutConstraint.activate([
  275. bg.widthAnchor.constraint(equalToConstant: 56),
  276. bg.heightAnchor.constraint(equalToConstant: 56),
  277. iv.centerXAnchor.constraint(equalTo: bg.centerXAnchor),
  278. iv.centerYAnchor.constraint(equalTo: bg.centerYAnchor),
  279. iv.widthAnchor.constraint(equalToConstant: 40),
  280. iv.heightAnchor.constraint(equalToConstant: 40)
  281. ])
  282. let label = UILabel(); label.text = title; label.font = .systemFont(ofSize: 12); label.textColor = UIColor(hex: "#6B6B6B")
  283. container.addArrangedSubview(bg)
  284. container.addArrangedSubview(label)
  285. // Full-size invisible button to capture taps
  286. let tap = UIButton(type: .system)
  287. tap.backgroundColor = .clear
  288. tap.accessibilityLabel = title // 把事件名称带出来
  289. tap.addTarget(self, action: #selector(onIconTapped(_:)), for: .touchUpInside)
  290. holder.addSubview(tap)
  291. tap.translatesAutoresizingMaskIntoConstraints = false
  292. NSLayoutConstraint.activate([
  293. tap.topAnchor.constraint(equalTo: holder.topAnchor),
  294. tap.bottomAnchor.constraint(equalTo: holder.bottomAnchor),
  295. tap.leadingAnchor.constraint(equalTo: holder.leadingAnchor),
  296. tap.trailingAnchor.constraint(equalTo: holder.trailingAnchor)
  297. ])
  298. return holder
  299. }
  300. // MARK: - Back/Editor
  301. @objc private func onBackTapped() {
  302. if let ed = editorView, !ed.isHidden {
  303. // Back from editor to grid
  304. toggleEditor(false, animated: true)
  305. } else {
  306. hide()
  307. }
  308. }
  309. @objc private func openQuickMemoEditor() {
  310. buildQuickMemoEditorIfNeeded()
  311. toggleEditor(true, animated: true)
  312. }
  313. private func buildQuickMemoEditorIfNeeded() {
  314. guard !editorBuilt else { return }
  315. editorBuilt = true
  316. //键盘回收功能
  317. wire()
  318. let ed = UIView()
  319. ed.backgroundColor = .clear
  320. ed.isHidden = true
  321. sheet.addSubview(ed)
  322. ed.translatesAutoresizingMaskIntoConstraints = false
  323. NSLayoutConstraint.activate([
  324. ed.leadingAnchor.constraint(equalTo: sheet.leadingAnchor, constant: 0),
  325. ed.trailingAnchor.constraint(equalTo: sheet.trailingAnchor, constant: 0),
  326. ed.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 0),
  327. ed.bottomAnchor.constraint(equalTo: sheet.bottomAnchor, constant: 0)
  328. ])
  329. self.editorView = ed
  330. // Card
  331. let card = UIView()
  332. card.backgroundColor = .white
  333. card.layer.cornerRadius = 14
  334. card.layer.masksToBounds = false
  335. ed.addSubview(card)
  336. card.translatesAutoresizingMaskIntoConstraints = false
  337. NSLayoutConstraint.activate([
  338. card.leadingAnchor.constraint(equalTo: ed.leadingAnchor, constant: 0),
  339. card.trailingAnchor.constraint(equalTo: ed.trailingAnchor, constant: -0),
  340. card.topAnchor.constraint(equalTo: ed.topAnchor, constant: 0)
  341. ])
  342. // Shadow to mimic design
  343. // card.layer.shadowColor = UIColor.black.withAlphaComponent(0.06).cgColor
  344. // card.layer.shadowOpacity = 1
  345. // card.layer.shadowRadius = 10
  346. // card.layer.shadowOffset = CGSize(width: 0, height: 6)
  347. let vstack = UIStackView(); vstack.axis = .vertical; vstack.alignment = .fill; vstack.spacing = 16
  348. card.addSubview(vstack)
  349. vstack.translatesAutoresizingMaskIntoConstraints = false
  350. NSLayoutConstraint.activate([
  351. vstack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  352. vstack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
  353. vstack.topAnchor.constraint(equalTo: card.topAnchor, constant: 16),
  354. ])
  355. // Icon + title
  356. let icon = UIImageView(image: UIImage(named: "peihead"))
  357. icon.contentMode = .scaleAspectFit
  358. icon.translatesAutoresizingMaskIntoConstraints = false
  359. icon.widthAnchor.constraint(equalToConstant: 88).isActive = true
  360. icon.heightAnchor.constraint(equalToConstant: 88).isActive = true
  361. let iconWrap = UIView(); iconWrap.translatesAutoresizingMaskIntoConstraints = false
  362. iconWrap.addSubview(icon)
  363. NSLayoutConstraint.activate([
  364. icon.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor),
  365. icon.topAnchor.constraint(equalTo: iconWrap.topAnchor),
  366. icon.bottomAnchor.constraint(equalTo: iconWrap.bottomAnchor)
  367. ])
  368. vstack.addArrangedSubview(iconWrap)
  369. // 修改标题行的创建代码
  370. let titleRow = UIStackView()
  371. titleRow.axis = .horizontal
  372. titleRow.alignment = .center
  373. titleRow.distribution = .fill
  374. editorTitleLabel.text = selectedEventName
  375. editorTitleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
  376. editorTitleLabel.textColor = UIColor(hex: "#2B2B2B")
  377. editorTitleLabel.textAlignment = .center // 添加居中文本对齐
  378. // 创建左右占位视图以确保标题居中
  379. let leftSpacer = UIView()
  380. let rightSpacer = UIView()
  381. titleRow.addArrangedSubview(leftSpacer)
  382. titleRow.addArrangedSubview(editorTitleLabel)
  383. titleRow.addArrangedSubview(rightSpacer)
  384. // 现在左右占位视图已经添加到同一个父视图(titleRow)中,可以设置宽度相等约束
  385. leftSpacer.widthAnchor.constraint(equalTo: rightSpacer.widthAnchor).isActive = true
  386. // 确保占位视图有最小高度
  387. leftSpacer.heightAnchor.constraint(equalToConstant: 44).isActive = true
  388. rightSpacer.heightAnchor.constraint(equalToConstant: 44).isActive = true
  389. // 标签按钮放在右侧占位视图中
  390. rightSpacer.addSubview(tagButton)
  391. rightSpacer.isUserInteractionEnabled = true
  392. tagButton.translatesAutoresizingMaskIntoConstraints = false
  393. tagButton.setImage(UIImage(named: "AddRecord382")?.withRenderingMode(.alwaysOriginal), for: .normal)
  394. tagButton.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
  395. tagButton.addTarget(self, action: #selector(openTagPicker), for: .touchUpInside)
  396. // 修改约束,确保按钮在右侧并居中
  397. NSLayoutConstraint.activate([
  398. tagButton.leadingAnchor.constraint(equalTo: rightSpacer.leadingAnchor, constant: 8), // 添加一些右边距
  399. tagButton.centerYAnchor.constraint(equalTo: rightSpacer.centerYAnchor),
  400. tagButton.widthAnchor.constraint(equalToConstant: 30), // 明确设置宽度
  401. tagButton.heightAnchor.constraint(equalToConstant: 30) // 明确设置高度
  402. ])
  403. vstack.addArrangedSubview(titleRow)
  404. // Form rows container
  405. rows = UIStackView(); rows.axis = .vertical; rows.spacing = 12
  406. vstack.addArrangedSubview(rows)
  407. // Row: 选择宠物(右侧小头像)Home372
  408. let petRow = makeFormRow(left: "选择宠物")
  409. petAvatar = UIImageView(image: UIImage(named: "Home372"))
  410. petAvatar.layer.cornerRadius = 16
  411. petAvatar.clipsToBounds = true
  412. petAvatar.translatesAutoresizingMaskIntoConstraints = false
  413. petAvatar.widthAnchor.constraint(equalToConstant: 32).isActive = true
  414. petAvatar.heightAnchor.constraint(equalToConstant: 32).isActive = true
  415. // Configure petNameLabel
  416. petNameLabel.font = .systemFont(ofSize: 14)
  417. petNameLabel.textColor = UIColor(hex: "#2B2B2B")
  418. petNameLabel.textAlignment = .right
  419. petNameLabel.numberOfLines = 1
  420. petNameLabel.text = ""
  421. petNameLabel.translatesAutoresizingMaskIntoConstraints = false
  422. // Set content hugging and compression priorities
  423. petNameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
  424. petNameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  425. petRow.rightContainer.addSubview(petNameLabel)
  426. petRow.rightContainer.addSubview(petAvatar)
  427. NSLayoutConstraint.activate([
  428. petAvatar.trailingAnchor.constraint(equalTo: petRow.rightContainer.trailingAnchor),
  429. petAvatar.centerYAnchor.constraint(equalTo: petRow.rightContainer.centerYAnchor),
  430. petNameLabel.trailingAnchor.constraint(equalTo: petAvatar.leadingAnchor, constant: -8),
  431. petNameLabel.centerYAnchor.constraint(equalTo: petRow.rightContainer.centerYAnchor),
  432. petNameLabel.leadingAnchor.constraint(greaterThanOrEqualTo: petRow.rightContainer.leadingAnchor)
  433. ])
  434. petRow.container.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(openPetPicker)))
  435. rows.addArrangedSubview(petRow.container)
  436. // Row: 记录时间(右侧日期 + chevron)
  437. let timeRow = makeFormRow(left: "记录时间")
  438. dateLabel = UILabel(); dateLabel.text = DateFormatter.memoDateString(from: Date()); dateLabel.font = .systemFont(ofSize: 14); dateLabel.textColor = UIColor(hex: "#2B2B2B")
  439. let chevron = UIImageView(image: UIImage(systemName: "chevron.right")); chevron.tintColor = UIColor(hex: "#2B2B2B"); chevron.translatesAutoresizingMaskIntoConstraints = false; chevron.widthAnchor.constraint(equalToConstant: 10).isActive = true
  440. let rightStack = UIStackView(); rightStack.axis = .horizontal; rightStack.alignment = .center; rightStack.spacing = 6
  441. rightStack.addArrangedSubview(dateLabel)
  442. rightStack.addArrangedSubview(chevron)
  443. timeRow.rightContainer.addSubview(rightStack)
  444. rightStack.translatesAutoresizingMaskIntoConstraints = false
  445. NSLayoutConstraint.activate([
  446. rightStack.trailingAnchor.constraint(equalTo: timeRow.rightContainer.trailingAnchor),
  447. rightStack.centerYAnchor.constraint(equalTo: timeRow.rightContainer.centerYAnchor)
  448. ])
  449. timeRow.container.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(openDatePicker)))
  450. rows.addArrangedSubview(timeRow.container)
  451. // Note box
  452. let noteBox = UIView(); noteBox.backgroundColor = UIColor(hex: "#F8F6F4"); noteBox.layer.cornerRadius = 12
  453. noteBox.translatesAutoresizingMaskIntoConstraints = false
  454. noteBox.heightAnchor.constraint(equalToConstant: 136).isActive = true
  455. vstack.addArrangedSubview(noteBox)
  456. editorTextView.backgroundColor = .clear
  457. editorTextView.font = .systemFont(ofSize: 14)
  458. editorTextView.textColor = UIColor(hex: "#2B2B2B")
  459. editorTextView.delegate = self
  460. noteBox.addSubview(editorTextView)
  461. editorTextView.translatesAutoresizingMaskIntoConstraints = false
  462. NSLayoutConstraint.activate([
  463. editorTextView.leadingAnchor.constraint(equalTo: noteBox.leadingAnchor, constant: 12),
  464. editorTextView.trailingAnchor.constraint(equalTo: noteBox.trailingAnchor, constant: -12),
  465. editorTextView.topAnchor.constraint(equalTo: noteBox.topAnchor, constant: 12),
  466. editorTextView.bottomAnchor.constraint(equalTo: noteBox.bottomAnchor, constant: -12)
  467. ])
  468. editorPlaceholder.text = "字里行间,都是我们的故事"
  469. editorPlaceholder.textColor = UIColor(hex: "#B0B0B0")
  470. editorPlaceholder.font = .systemFont(ofSize: 14)
  471. noteBox.addSubview(editorPlaceholder)
  472. editorPlaceholder.translatesAutoresizingMaskIntoConstraints = false
  473. NSLayoutConstraint.activate([
  474. editorPlaceholder.leadingAnchor.constraint(equalTo: editorTextView.leadingAnchor),
  475. editorPlaceholder.topAnchor.constraint(equalTo: editorTextView.topAnchor)
  476. ])
  477. // Save button
  478. let saveBtn = UIButton(type: .system)
  479. saveBtn.setTitle("保存", for: .normal)
  480. saveBtn.setTitleColor(.black, for: .normal)
  481. saveBtn.backgroundColor = UIColor(hex: "#FFE059")
  482. saveBtn.layer.cornerRadius = 24
  483. vstack.addArrangedSubview(saveBtn)
  484. saveBtn.heightAnchor.constraint(equalToConstant: 48).isActive = true
  485. saveBtn.addTarget(self, action: #selector(onSaveMemo), for: .touchUpInside)
  486. // Bottom spacing to allow scrolling content not clipped
  487. let bottom = UIView(); bottom.heightAnchor.constraint(equalToConstant: 16).isActive = true
  488. vstack.addArrangedSubview(bottom)
  489. // Lay out mini dash square in next layout pass
  490. // mini.layoutIfNeeded()
  491. // let miniInset: CGFloat = 0.5
  492. // let miniPath = UIBezierPath(roundedRect: mini.bounds.insetBy(dx: miniInset, dy: miniInset), cornerRadius: 3)
  493. // miniDash.path = miniPath.cgPath
  494. // Ensure card bottom anchors to its subviews
  495. let bottomConstraint = vstack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16)
  496. bottomConstraint.isActive = true
  497. }
  498. @objc private func openPetPicker() {
  499. let petListVC = PetListViewController()
  500. petListVC.onPetSelected = { [weak self] selectedPet in
  501. self?.updatePetInfo(pet: selectedPet)
  502. }
  503. viewController?.present(petListVC, animated: true)
  504. }
  505. func updatePetInfo(pet: Pet) {
  506. petNameLabel.text = pet.name
  507. // 在加载图片前设置selectedPetId
  508. self.selectedPetId = Int(pet.id) ?? self.selectedPetId
  509. print("dddd \(pet.name)")
  510. if let url = URL(string: pet.avatar) {
  511. loadImage(from: url)
  512. }
  513. // selectedPet = pet
  514. }
  515. private func loadImage(from url: URL) {
  516. // Load the image without using third-party libraries
  517. URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
  518. guard let data = data, error == nil else { return }
  519. DispatchQueue.main.async {
  520. self?.petAvatar.image = UIImage(data: data)
  521. }
  522. }.resume()
  523. }
  524. @objc private func openDatePicker() {
  525. let datePickerController = DatePickerSheetController(initial: selectedDate ?? Date())
  526. datePickerController.onDone = { [weak self] selectedDate in
  527. self?.selectedDate = selectedDate
  528. let dateFormatter = DateFormatter()
  529. dateFormatter.dateFormat = "yyyy-MM-dd"
  530. print("sss \(dateFormatter.string(from: selectedDate))")
  531. // 更新记录时间显示
  532. self?.dateLabel.text = dateFormatter.string(from: selectedDate)
  533. }
  534. viewController?.present(datePickerController, animated: false)
  535. }
  536. private func wire() {
  537. // 关闭键盘的手势
  538. let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
  539. tapGesture.cancelsTouchesInView = false
  540. addGestureRecognizer(tapGesture)
  541. }
  542. @objc private func dismissKeyboard() {
  543. // 结束编辑并收回键盘
  544. editorTextView.resignFirstResponder()
  545. }
  546. private func toggleEditor(_ show: Bool, animated: Bool) {
  547. guard let ed = editorView else { return }
  548. let work = {
  549. ed.isHidden = !show
  550. self.scroll.isHidden = show
  551. }
  552. if animated {
  553. UIView.transition(with: sheet, duration: 0.2, options: .transitionCrossDissolve, animations: work)
  554. } else { work() }
  555. }
  556. private func makeFormRow(left: String) -> (container: UIView, rightContainer: UIView) {
  557. let box = UIView(); box.backgroundColor = UIColor.white; box.layer.cornerRadius = 12
  558. box.layer.shadowColor = UIColor.black.withAlphaComponent(0.05).cgColor
  559. box.layer.shadowOpacity = 1; box.layer.shadowRadius = 6; box.layer.shadowOffset = CGSize(width: 0, height: 3)
  560. let leftLabel = UILabel(); leftLabel.text = left; leftLabel.font = .systemFont(ofSize: 14); leftLabel.textColor = UIColor(hex: "#6B6B6B")
  561. let right = UIView()
  562. let line = UIView(); line.backgroundColor = UIColor(hex: "#F0EEEC")
  563. [leftLabel, right, line].forEach { box.addSubview($0); $0.translatesAutoresizingMaskIntoConstraints = false }
  564. NSLayoutConstraint.activate([
  565. leftLabel.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 12),
  566. leftLabel.centerYAnchor.constraint(equalTo: box.centerYAnchor),
  567. right.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -12),
  568. right.centerYAnchor.constraint(equalTo: box.centerYAnchor),
  569. right.leadingAnchor.constraint(greaterThanOrEqualTo: leftLabel.trailingAnchor, constant: 8),
  570. line.heightAnchor.constraint(equalToConstant: 1),
  571. line.leadingAnchor.constraint(equalTo: box.leadingAnchor),
  572. line.trailingAnchor.constraint(equalTo: box.trailingAnchor),
  573. line.bottomAnchor.constraint(equalTo: box.bottomAnchor)
  574. ])
  575. box.heightAnchor.constraint(equalToConstant: 54).isActive = true
  576. // 为 container 添加手势识别器
  577. let container = box
  578. let rightContainer = right
  579. container.isUserInteractionEnabled = true // 确保视图可以接受手势
  580. return (container, rightContainer)
  581. }
  582. @objc private func onSaveMemo() {
  583. // 1. 构建请求body
  584. let content = editorTextView.text ?? ""
  585. let title = (editorTitleLabel.text?.isEmpty == false ? editorTitleLabel.text! : selectedEventName)
  586. let dateFormatter = DateFormatter()
  587. dateFormatter.dateFormat = "yyyy-MM-dd"
  588. let recordTypeIdVal = recordTypeMap[title] ?? 0
  589. let recordDate = dateFormatter.string(from: selectedDate ?? Date())
  590. let petId = selectedPetId ?? 0
  591. let params: [String: Any] = [
  592. "content": content,
  593. "title": title,
  594. "recordDate": recordDate,
  595. "petId": petId,
  596. // 其他字段可按接口需要补充,暂用默认
  597. "module": "event",
  598. "recordTypeId": recordTypeIdVal,
  599. "recordUrl": "",
  600. "id": 0,
  601. "createTime": "",
  602. "updateTime": ""
  603. ]
  604. print("🧩 recordTypeId for title(\(title)) = \(recordTypeIdVal)")
  605. guard let url = URL(string: "\(baseURL)/petRecordInfo/createRecord") else {
  606. showToast("无效的请求地址")
  607. return
  608. }
  609. var request = URLRequest(url: url)
  610. request.httpMethod = "POST"
  611. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  612. if let token = UserDefaults.standard.string(forKey: "userToken") {
  613. request.setValue(token, forHTTPHeaderField: "Authorization")
  614. }
  615. do {
  616. let body = try JSONSerialization.data(withJSONObject: params, options: [])
  617. request.httpBody = body
  618. print("请求参数: \(String(data: body, encoding: .utf8) ?? "")")
  619. } catch {
  620. showToast("参数序列化失败")
  621. return
  622. }
  623. let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
  624. DispatchQueue.main.async {
  625. if let error = error {
  626. print("网络错误: \(error)")
  627. self?.showToast("网络错误")
  628. return
  629. }
  630. guard let httpResp = response as? HTTPURLResponse else {
  631. self?.showToast("无响应")
  632. return
  633. }
  634. let status = httpResp.statusCode
  635. let respBody = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
  636. print("响应状态: \(status)")
  637. print("响应内容: \(respBody)")
  638. // 尝试解析json
  639. var codeValue: String = ""
  640. var msgValue: String = ""
  641. if let d = data {
  642. if let json = try? JSONSerialization.jsonObject(with: d, options: []) as? [String: Any] {
  643. codeValue = (json["code"] as? String) ?? (json["code"] as? Int).map { String($0) } ?? ""
  644. msgValue = (json["msg"] as? String) ?? ""
  645. }
  646. }
  647. if status == 200 && (codeValue == "200" || codeValue == "200") {
  648. self?.showToast("保存成功")
  649. // 通知 HomeViewController 刷新数据
  650. let petIdStr = String(petId)
  651. NotificationCenter.default.post(name: .eventRecordDidCreate,
  652. object: nil,
  653. userInfo: [
  654. "petId": petIdStr,
  655. "recordDate": recordDate,
  656. "title": title
  657. ])
  658. DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
  659. self?.hide()
  660. }
  661. } else {
  662. self?.showToast(msgValue.isEmpty ? "保存失败" : msgValue)
  663. }
  664. }
  665. }
  666. task.resume()
  667. }
  668. // MARK: - Toast Helper
  669. private func showToast(_ text: String) {
  670. let lab = PaddingLabel(insets: .init(top: 8, left: 12, bottom: 8, right: 12))
  671. lab.backgroundColor = UIColor.black.withAlphaComponent(0.85)
  672. lab.textColor = .white
  673. lab.font = .systemFont(ofSize: 14)
  674. lab.layer.cornerRadius = 8
  675. lab.layer.masksToBounds = true
  676. lab.text = text
  677. sheet.addSubview(lab)
  678. lab.translatesAutoresizingMaskIntoConstraints = false
  679. NSLayoutConstraint.activate([
  680. lab.centerXAnchor.constraint(equalTo: sheet.centerXAnchor),
  681. lab.bottomAnchor.constraint(equalTo: sheet.bottomAnchor, constant: -40)
  682. ])
  683. lab.alpha = 0
  684. UIView.animate(withDuration: 0.2, animations: { lab.alpha = 1 }) { _ in
  685. UIView.animate(withDuration: 0.2, delay: 1.2, options: [], animations: { lab.alpha = 0 }) { _ in
  686. lab.removeFromSuperview()
  687. }
  688. }
  689. }
  690. // MARK: - Memo & Icon Tap Handlers
  691. @objc private func onMemoTapped() {
  692. selectedEventName = "随手小记"
  693. editorTitleLabel.text = selectedEventName
  694. openQuickMemoEditor()
  695. }
  696. @objc private func onIconTapped(_ sender: UIButton) {
  697. let name = sender.accessibilityLabel ?? "随手小记"
  698. selectedEventName = name
  699. editorTitleLabel.text = name
  700. openQuickMemoEditor()
  701. }
  702. // MARK: - Tag Picker UI
  703. @objc private func openTagPicker() {
  704. tagPickerView.isHidden = false
  705. tagPickerView.show()
  706. }
  707. func textViewDidChange(_ textView: UITextView) {
  708. editorPlaceholder.isHidden = !textView.text.isEmpty
  709. }
  710. }
  711. extension DateFormatter {
  712. static func memoDateString(from date: Date) -> String {
  713. let df = DateFormatter(); df.dateFormat = "yyyy-MM-dd"; return df.string(from: date)
  714. }
  715. }
  716. // MARK: - Notification Keys
  717. extension Notification.Name {
  718. static let eventRecordDidCreate = Notification.Name("EventRecordDidCreate")
  719. }