LNCreateOrderViewController.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. //
  2. // LNCreateOrderViewController.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. import Combine
  11. extension UIView {
  12. func pushToCreateOrder(_ skillDetail: LNGameMateSkillDetailVO, count: Int = 1) {
  13. let vc = LNCreateOrderViewController()
  14. vc.loadSkill(skillDetail, count: count)
  15. navigationController?.pushViewController(vc, animated: true)
  16. }
  17. func pushToCreateOrder(_ qrCode: LNQRCodeDetailResponse) {
  18. let vc = LNCreateOrderViewController()
  19. vc.loadQRData(qrCode)
  20. navigationController?.pushViewController(vc, animated: true)
  21. }
  22. }
  23. class LNCreateOrderViewController: LNViewController {
  24. private var skill: LNGameMateSkillDetailVO?
  25. private var qrDetail: LNQRCodeDetailResponse?
  26. private let avatar = UIImageView()
  27. private let nameLabel = UILabel()
  28. private let starLabel = UILabel()
  29. private let skillIc = UIImageView()
  30. private let skillNameLabel = UILabel()
  31. private let skillPriceLabel = UILabel()
  32. private let skillUnitLabel = UILabel()
  33. private let countLabel = UILabel()
  34. private let minuButton = UIButton()
  35. private let addButton = UIButton()
  36. private var curCount = 1 {
  37. didSet {
  38. countLabel.text = "\(curCount)"
  39. if curCount <= 1 {
  40. minuButton.isEnabled = false
  41. } else {
  42. minuButton.isEnabled = true
  43. }
  44. updateCost()
  45. }
  46. }
  47. private let extraInput = UITextField()
  48. private let costLabel = UILabel()
  49. private let orderButton = UIButton()
  50. private let emptyDiscountView = UIView()
  51. private let newbieDiscountView = UIView()
  52. private let discountView = LNNewbieDiscountView()
  53. private let discountCoinLabel = UILabel()
  54. override func viewDidLoad() {
  55. super.viewDidLoad()
  56. setupViews()
  57. }
  58. func loadSkill(_ detail: LNGameMateSkillDetailVO, count: Int) {
  59. skill = detail
  60. updateContent(detail)
  61. curCount = count
  62. }
  63. func loadQRData(_ data: LNQRCodeDetailResponse) {
  64. qrDetail = data
  65. updateContent(data)
  66. }
  67. }
  68. extension LNCreateOrderViewController: UITextFieldDelegate {
  69. func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  70. let currentText = textField.text ?? ""
  71. guard let range = Range(range, in: currentText) else { return false }
  72. let newText = currentText.replacingCharacters(in: range, with: string)
  73. if newText.count < currentText.count {
  74. return true
  75. }
  76. return newText.count <= LNOrderManager.orderExtraMaxLength
  77. }
  78. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  79. textField.resignFirstResponder()
  80. return true
  81. }
  82. }
  83. extension LNCreateOrderViewController {
  84. private func updateContent(_ detail: LNGameMateSkillDetailVO) {
  85. avatar.showAvatar(detail.avatar)
  86. nameLabel.text = detail.nickname
  87. starLabel.text = "\(detail.star)"
  88. let skill = LNGameMateManager.shared.gameCategory(for: detail.categoryCode)
  89. skillIc.sd_setImage(with: URL(string: skill?.icon ?? ""))
  90. skillNameLabel.text = detail.categoryName
  91. skillPriceLabel.text = "\(detail.price.toDisplay)"
  92. skillUnitLabel.text = "/\(detail.unit)"
  93. curCount = 1
  94. if let discount = LNOrderManager.shared.discountFor(detail.price) {
  95. emptyDiscountView.isHidden = true
  96. newbieDiscountView.isHidden = false
  97. discountView.update(1 - discount)
  98. discountCoinLabel.text = "-\((1 - discount) * detail.price)"
  99. } else {
  100. emptyDiscountView.isHidden = false
  101. newbieDiscountView.isHidden = true
  102. }
  103. orderButton.isEnabled = true
  104. }
  105. private func updateContent(_ detail: LNQRCodeDetailResponse) {
  106. avatar.showAvatar(detail.avatar)
  107. nameLabel.text = detail.nickname
  108. starLabel.text = "\(detail.star)"
  109. skillIc.sd_setImage(with: URL(string: detail.bizCategoryIcon))
  110. skillNameLabel.text = detail.bizCategoryName
  111. skillPriceLabel.text = "\(detail.price.toDisplay)"
  112. skillUnitLabel.text = "/\(detail.unit)"
  113. curCount = detail.purchaseQty
  114. if detail.type != .normal {
  115. minuButton.isEnabled = false
  116. addButton.isEnabled = false
  117. }
  118. if let discount = LNOrderManager.shared.discountFor(detail.price) {
  119. emptyDiscountView.isHidden = true
  120. newbieDiscountView.isHidden = false
  121. discountView.update(1 - discount)
  122. discountCoinLabel.text = "-\((1 - discount) * detail.price)"
  123. } else {
  124. emptyDiscountView.isHidden = false
  125. newbieDiscountView.isHidden = true
  126. }
  127. orderButton.isEnabled = !detail.sellerUserNo.isMyUid
  128. }
  129. private func updateCost() {
  130. let price: Double = if let skill {
  131. skill.price
  132. } else if let qrDetail {
  133. qrDetail.price
  134. } else {
  135. 0
  136. }
  137. let cost = price * Double(curCount)
  138. if let discount = LNOrderManager.shared.discountFor(price) {
  139. costLabel.text = (cost - (1 - discount) * price).toDisplay
  140. } else {
  141. costLabel.text = cost.toDisplay
  142. }
  143. // orderUnitLabel.text = "/\(curCount) \(unit)"
  144. }
  145. private func setupViews() {
  146. title = .init(key: "A00127")
  147. view.backgroundColor = .primary_1
  148. view.onTap { [weak self] in
  149. guard let self else { return }
  150. view.endEditing(true)
  151. }
  152. let stackView = UIStackView()
  153. stackView.axis = .vertical
  154. stackView.spacing = 10
  155. view.addSubview(stackView)
  156. stackView.snp.makeConstraints { make in
  157. make.horizontalEdges.equalToSuperview().inset(16)
  158. make.top.equalToSuperview().offset(16)
  159. }
  160. stackView.addArrangedSubview(buildUserInfo())
  161. stackView.addArrangedSubview(buildSkillInfo())
  162. stackView.addArrangedSubview(buildExtra())
  163. let menu = buildMenu()
  164. view.addSubview(menu)
  165. menu.snp.makeConstraints { make in
  166. make.horizontalEdges.equalToSuperview()
  167. make.bottom.equalToSuperview()
  168. }
  169. }
  170. private func buildUserInfo() -> UIView {
  171. let container = UIView()
  172. container.backgroundColor = .fill
  173. container.layer.cornerRadius = 12
  174. container.snp.makeConstraints { make in
  175. make.height.equalTo(60)
  176. }
  177. let starView = UIView()
  178. container.addSubview(starView)
  179. starView.snp.makeConstraints { make in
  180. make.centerY.equalToSuperview()
  181. make.trailing.equalToSuperview().offset(-12)
  182. }
  183. let star = UIImageView()
  184. star.image = .icStarFill
  185. starView.addSubview(star)
  186. star.snp.makeConstraints { make in
  187. make.centerY.equalToSuperview()
  188. make.leading.equalToSuperview()
  189. make.width.height.equalTo(16)
  190. }
  191. starLabel.font = .heading_h3
  192. starLabel.textColor = .text_5
  193. starView.addSubview(starLabel)
  194. starLabel.snp.makeConstraints { make in
  195. make.verticalEdges.equalToSuperview()
  196. make.leading.equalTo(star.snp.trailing).offset(4)
  197. make.trailing.equalToSuperview()
  198. }
  199. avatar.layer.cornerRadius = 20
  200. avatar.clipsToBounds = true
  201. container.addSubview(avatar)
  202. avatar.snp.makeConstraints { make in
  203. make.leading.equalToSuperview().offset(12)
  204. make.centerY.equalToSuperview()
  205. make.width.height.equalTo(40)
  206. }
  207. nameLabel.font = .heading_h4
  208. nameLabel.textColor = .text_5
  209. container.addSubview(nameLabel)
  210. nameLabel.snp.makeConstraints { make in
  211. make.leading.equalTo(avatar.snp.trailing).offset(8)
  212. make.centerY.equalToSuperview()
  213. make.trailing.lessThanOrEqualTo(starView.snp.leading).offset(-12)
  214. }
  215. return container
  216. }
  217. private func buildSkillInfo() -> UIView {
  218. let container = UIStackView()
  219. container.axis = .vertical
  220. container.spacing = 0
  221. container.backgroundColor = .fill
  222. container.layer.cornerRadius = 12
  223. container.addArrangedSubview(buildInfoView())
  224. container.addArrangedSubview(buildPriceView())
  225. container.addArrangedSubview(buildCountView())
  226. container.addArrangedSubview(buildDiscount())
  227. let emptyView = UIView()
  228. emptyView.snp.makeConstraints { make in
  229. make.height.equalTo(10)
  230. }
  231. container.addArrangedSubview(emptyView)
  232. return container
  233. }
  234. private func buildInfoView() -> UIView {
  235. let infoView = UIView()
  236. infoView.snp.makeConstraints { make in
  237. make.height.equalTo(40)
  238. }
  239. skillIc.layer.cornerRadius = 13
  240. skillIc.clipsToBounds = true
  241. infoView.addSubview(skillIc)
  242. skillIc.snp.makeConstraints { make in
  243. make.leading.equalToSuperview().offset(12)
  244. make.centerY.equalToSuperview()
  245. make.width.height.equalTo(26)
  246. }
  247. skillNameLabel.font = .heading_h5
  248. skillNameLabel.textColor = .text_5
  249. infoView.addSubview(skillNameLabel)
  250. skillNameLabel.snp.makeConstraints { make in
  251. make.centerY.equalToSuperview()
  252. make.leading.equalTo(skillIc.snp.trailing).offset(2)
  253. }
  254. let line = UIView()
  255. line.backgroundColor = .fill_2
  256. infoView.addSubview(line)
  257. line.snp.makeConstraints { make in
  258. make.horizontalEdges.equalToSuperview().inset(11)
  259. make.bottom.equalToSuperview()
  260. make.height.equalTo(1)
  261. }
  262. return infoView
  263. }
  264. private func buildPriceView() -> UIView {
  265. let priceView = UIView()
  266. priceView.snp.makeConstraints { make in
  267. make.height.equalTo(38)
  268. }
  269. let priceTitleLabel = UILabel()
  270. priceTitleLabel.font = .heading_h5
  271. priceTitleLabel.textColor = .text_5
  272. priceTitleLabel.text = .init(key: "A00128")
  273. priceView.addSubview(priceTitleLabel)
  274. priceTitleLabel.snp.makeConstraints { make in
  275. make.leading.equalToSuperview().offset(12)
  276. make.centerY.equalToSuperview()
  277. }
  278. skillUnitLabel.font = .body_s
  279. skillUnitLabel.textColor = .text_4
  280. priceView.addSubview(skillUnitLabel)
  281. skillUnitLabel.snp.makeConstraints { make in
  282. make.centerY.equalToSuperview()
  283. make.trailing.equalToSuperview().offset(-12)
  284. }
  285. skillPriceLabel.font = .heading_h3
  286. skillPriceLabel.textColor = .text_5
  287. priceView.addSubview(skillPriceLabel)
  288. skillPriceLabel.snp.makeConstraints { make in
  289. make.centerY.equalToSuperview()
  290. make.trailing.equalTo(skillUnitLabel.snp.leading)
  291. }
  292. let coinIc = UIImageView.coinImageView()
  293. priceView.addSubview(coinIc)
  294. coinIc.snp.makeConstraints { make in
  295. make.centerY.equalToSuperview()
  296. make.trailing.equalTo(skillPriceLabel.snp.leading).offset(-2)
  297. make.width.height.equalTo(20)
  298. }
  299. return priceView
  300. }
  301. private func buildCountView() -> UIView {
  302. let countView = UIView()
  303. countView.snp.makeConstraints { make in
  304. make.height.equalTo(38)
  305. }
  306. let countTitleLabel = UILabel()
  307. countTitleLabel.font = .heading_h5
  308. countTitleLabel.textColor = .text_5
  309. countTitleLabel.text = .init(key: "A00122")
  310. countView.addSubview(countTitleLabel)
  311. countTitleLabel.snp.makeConstraints { make in
  312. make.leading.equalToSuperview().offset(12)
  313. make.centerY.equalToSuperview()
  314. }
  315. addButton.setTitle("+", for: .normal)
  316. addButton.setTitleColor(.text_4, for: .normal)
  317. addButton.setTitleColor(.text_2, for: .disabled)
  318. addButton.backgroundColor = .primary_1
  319. addButton.layer.cornerRadius = 12
  320. addButton.addAction(UIAction(handler: { [weak self] _ in
  321. guard let self else { return }
  322. curCount += 1
  323. }), for: .touchUpInside)
  324. countView.addSubview(addButton)
  325. addButton.snp.makeConstraints { make in
  326. make.centerY.equalToSuperview()
  327. make.trailing.equalToSuperview().offset(-12)
  328. make.width.height.equalTo(24)
  329. }
  330. countLabel.font = .body_m
  331. countLabel.textColor = .text_5
  332. countView.addSubview(countLabel)
  333. countLabel.snp.makeConstraints { make in
  334. make.centerY.equalToSuperview()
  335. make.trailing.equalTo(addButton.snp.leading).offset(-10)
  336. }
  337. minuButton.setTitle("-", for: .normal)
  338. minuButton.setTitleColor(.text_4, for: .normal)
  339. minuButton.setTitleColor(.text_2, for: .disabled)
  340. minuButton.backgroundColor = .primary_1
  341. minuButton.layer.cornerRadius = 12
  342. minuButton.addAction(UIAction(handler: { [weak self] _ in
  343. guard let self else { return }
  344. self.curCount -= 1
  345. }), for: .touchUpInside)
  346. countView.addSubview(minuButton)
  347. minuButton.snp.makeConstraints { make in
  348. make.centerY.equalToSuperview()
  349. make.trailing.equalTo(countLabel.snp.leading).offset(-10)
  350. make.width.height.equalTo(24)
  351. }
  352. return countView
  353. }
  354. private func buildDiscount() -> UIView {
  355. let discountView = UIView()
  356. discountView.snp.makeConstraints { make in
  357. make.height.equalTo(38)
  358. }
  359. let discountTitleLabel = UILabel()
  360. discountTitleLabel.font = .heading_h5
  361. discountTitleLabel.textColor = .text_5
  362. discountTitleLabel.text = .init(key: "A00129")
  363. discountView.addSubview(discountTitleLabel)
  364. discountTitleLabel.snp.makeConstraints { make in
  365. make.leading.equalToSuperview().offset(12)
  366. make.centerY.equalToSuperview()
  367. }
  368. emptyDiscountView.isHidden = true
  369. discountView.addSubview(emptyDiscountView)
  370. emptyDiscountView.snp.makeConstraints { make in
  371. make.verticalEdges.equalToSuperview()
  372. make.trailing.equalToSuperview()
  373. }
  374. let discountArrow = UIImageView.arrowImageView(size: 14)
  375. discountArrow.tintColor = .text_2
  376. emptyDiscountView.addSubview(discountArrow)
  377. discountArrow.snp.makeConstraints { make in
  378. make.centerY.equalToSuperview()
  379. make.trailing.equalToSuperview().offset(-12)
  380. }
  381. let discountDescLabel = UILabel()
  382. discountDescLabel.font = .body_s
  383. discountDescLabel.text = .init(key: "A00130")
  384. discountDescLabel.textColor = .text_2
  385. emptyDiscountView.addSubview(discountDescLabel)
  386. discountDescLabel.snp.makeConstraints { make in
  387. make.centerY.equalToSuperview()
  388. make.trailing.equalTo(discountArrow.snp.leading).offset(-3)
  389. make.leading.equalToSuperview()
  390. }
  391. let newbieView = buildNewbieDiscountView()
  392. discountView.addSubview(newbieView)
  393. newbieView.snp.makeConstraints { make in
  394. make.verticalEdges.equalToSuperview()
  395. make.trailing.equalToSuperview()
  396. }
  397. return discountView
  398. }
  399. private func buildNewbieDiscountView() -> UIView {
  400. discountCoinLabel.font = .heading_h3
  401. discountCoinLabel.textColor = .init(hex: "#FF6F32")
  402. newbieDiscountView.addSubview(discountCoinLabel)
  403. discountCoinLabel.snp.makeConstraints { make in
  404. make.centerY.equalToSuperview()
  405. make.trailing.equalToSuperview().offset(-10)
  406. }
  407. let coin = UIImageView(image: .icCoin)
  408. newbieDiscountView.addSubview(coin)
  409. coin.snp.makeConstraints { make in
  410. make.centerY.equalToSuperview()
  411. make.trailing.equalTo(discountCoinLabel.snp.leading).offset(-2)
  412. make.width.height.equalTo(20)
  413. }
  414. newbieDiscountView.addSubview(discountView)
  415. discountView.snp.makeConstraints { make in
  416. make.centerY.equalToSuperview()
  417. make.trailing.equalTo(coin.snp.leading).offset(-4)
  418. }
  419. return newbieDiscountView
  420. }
  421. private func buildExtra() -> UIView {
  422. let container = UIView()
  423. container.backgroundColor = .fill
  424. container.layer.cornerRadius = 12
  425. container.snp.makeConstraints { make in
  426. make.height.equalTo(50)
  427. }
  428. let titleLabel = UILabel()
  429. titleLabel.font = .heading_h5
  430. titleLabel.textColor = .text_5
  431. titleLabel.text = .init(key: "A00086")
  432. titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  433. container.addSubview(titleLabel)
  434. titleLabel.snp.makeConstraints { make in
  435. make.centerY.equalToSuperview()
  436. make.leading.equalToSuperview().offset(12)
  437. }
  438. extraInput.font = .body_s
  439. extraInput.textColor = .text_5
  440. extraInput.placeholder = .init(key: "A00125", LNOrderManager.orderExtraMaxLength)
  441. extraInput.clearButtonMode = .whileEditing
  442. extraInput.delegate = self
  443. container.addSubview(extraInput)
  444. extraInput.snp.makeConstraints { make in
  445. make.centerY.equalToSuperview()
  446. make.leading.equalTo(titleLabel.snp.trailing).offset(8)
  447. make.trailing.equalToSuperview().offset(-12)
  448. }
  449. return container
  450. }
  451. private func buildMenu() -> UIView {
  452. let holder = UIView()
  453. let bottomGradient = CAGradientLayer()
  454. bottomGradient.colors = [
  455. UIColor.white.withAlphaComponent(0).cgColor,
  456. UIColor.white.cgColor,
  457. UIColor.white.cgColor
  458. ]
  459. bottomGradient.locations = [0, 0.5, 1]
  460. bottomGradient.startPoint = .init(x: 0, y: 0)
  461. bottomGradient.endPoint = .init(x: 0, y: 1)
  462. holder.layer.addSublayer(bottomGradient)
  463. holder.publisher(for: \.bounds).removeDuplicates().sink { [weak bottomGradient] newValue in
  464. guard let bottomGradient else { return }
  465. bottomGradient.frame = newValue
  466. }.store(in: &cancellables)
  467. let container = UIView()
  468. container.backgroundColor = .init(hex: "#ECF6FF")
  469. container.layer.cornerRadius = 23.5
  470. holder.addSubview(container)
  471. container.snp.makeConstraints { make in
  472. make.horizontalEdges.equalToSuperview().inset(16)
  473. make.top.equalToSuperview().offset(16)
  474. make.bottom.equalToSuperview().offset(-view.safeBottomInset - 10)
  475. }
  476. orderButton.setBackgroundImage(.icSkillOrderLong, for: .normal)
  477. orderButton.setTitle(.init(key: "A00041"), for: .normal)
  478. orderButton.setTitleColor(.text_1, for: .normal)
  479. orderButton.titleLabel?.font = .heading_h3
  480. orderButton.addAction(UIAction(handler: { [weak self] _ in
  481. guard let self else { return }
  482. let panel = LNCreateOrderPanel()
  483. if let skill {
  484. panel.update(skill, count: curCount, extra: extraInput.text ?? "")
  485. } else if let qrDetail {
  486. panel.update(qrDetail, count: curCount, extra: extraInput.text ?? "")
  487. } else { return }
  488. panel.completionHandler = { [weak self] orderId in
  489. guard let self else { return }
  490. navigationController?.viewControllers.removeAll { $0 is LNCreateOrderViewController }
  491. }
  492. panel.popup()
  493. }), for: .touchUpInside)
  494. container.addSubview(orderButton)
  495. orderButton.snp.makeConstraints { make in
  496. make.trailing.equalToSuperview()
  497. make.verticalEdges.equalToSuperview()
  498. }
  499. let descView = UIView()
  500. container.addSubview(descView)
  501. descView.snp.makeConstraints { make in
  502. make.leading.verticalEdges.equalToSuperview()
  503. make.trailing.equalTo(orderButton.snp.leading)
  504. }
  505. let priceView = UIView()
  506. descView.addSubview(priceView)
  507. priceView.snp.makeConstraints { make in
  508. make.center.equalToSuperview()
  509. }
  510. let coin = UIImageView.coinImageView()
  511. priceView.addSubview(coin)
  512. coin.snp.makeConstraints { make in
  513. make.leading.centerY.equalToSuperview()
  514. make.width.height.equalTo(20)
  515. }
  516. costLabel.font = .heading_h2
  517. costLabel.textColor = .text_5
  518. priceView.addSubview(costLabel)
  519. costLabel.snp.makeConstraints { make in
  520. make.verticalEdges.equalToSuperview()
  521. make.leading.equalTo(coin.snp.trailing).offset(4)
  522. make.trailing.equalToSuperview()
  523. }
  524. return holder
  525. }
  526. }
  527. #if DEBUG
  528. import SwiftUI
  529. struct LNCreateOrderViewControllerPreview: UIViewControllerRepresentable {
  530. func makeUIViewController(context: Context) -> some UIViewController {
  531. LNCreateOrderViewController()
  532. }
  533. func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
  534. }
  535. #Preview(body: {
  536. LNCreateOrderViewControllerPreview()
  537. })
  538. #endif