LNCreateOrderPanel.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. //
  2. // LNCreateOrderPanel.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/22.
  6. //
  7. import Foundation
  8. import UIKit
  9. import SnapKit
  10. class LNCreateOrderPanel: LNPopupView {
  11. private let gameNameLabel = UILabel()
  12. private let avatar = UIImageView()
  13. private let nameLabel = UILabel()
  14. private let priceLabel = UILabel()
  15. private let countEditView = UIView()
  16. private let minuButton = UIButton()
  17. private let customCountLabel = UILabel()
  18. private let addButton = UIButton()
  19. private let countLabel = UILabel()
  20. private let costLabel = UILabel()
  21. private let discountView = UIView()
  22. private let newbieView = LNNewbieDiscountView()
  23. private let discountCoinLabel = UILabel()
  24. private let extraView = UIView()
  25. private let extraInput = UITextField()
  26. private let curCoinLabel = UILabel()
  27. private var skillId: String?
  28. private var qrCode: String?
  29. private var price: Double = 0.0
  30. private var curCount = 1 {
  31. didSet {
  32. countLabel.text = "x\(curCount)"
  33. customCountLabel.text = "\(curCount)"
  34. if let discount = LNOrderManager.shared.discountFor(price) {
  35. costLabel.text = (price * Double(curCount) - (1 - discount) * price).toDisplay
  36. } else {
  37. costLabel.text = (price * Double(curCount)).toDisplay
  38. }
  39. if curCount <= 1 {
  40. minuButton.isEnabled = false
  41. } else {
  42. minuButton.isEnabled = true
  43. }
  44. }
  45. }
  46. var editable: Bool = false {
  47. didSet {
  48. extraView.isHidden = !editable
  49. countEditView.isHidden = !editable
  50. countLabel.isHidden = editable
  51. }
  52. }
  53. var completionHandler: ((String) -> Void)?
  54. override init(frame: CGRect) {
  55. super.init(frame: frame)
  56. setupViews()
  57. LNEventDeliver.addObserver(self)
  58. }
  59. func update(_ detail: LNGameMateSkillDetailVO, count: Int, extra: String) {
  60. gameNameLabel.text = detail.categoryName
  61. avatar.showAvatar(detail.avatar)
  62. nameLabel.text = detail.nickname
  63. priceLabel.text = "\(detail.price.toDisplay)/\(detail.unit)"
  64. if let discount = LNOrderManager.shared.discountFor(detail.price) {
  65. discountView.isHidden = false
  66. newbieView.update(1 - discount)
  67. discountCoinLabel.text = "-\(detail.price * (1 - discount))"
  68. } else {
  69. discountView.isHidden = true
  70. }
  71. skillId = detail.id
  72. price = detail.price
  73. curCount = count
  74. extraInput.text = extra
  75. }
  76. func update(_ skill: LNGameMateSkillVO, user: LNUserProfileVO) {
  77. gameNameLabel.text = skill.name
  78. avatar.showAvatar(user.avatar)
  79. nameLabel.text = user.nickname
  80. priceLabel.text = "\(skill.price.toDisplay)/\(skill.unit)"
  81. if let discount = LNOrderManager.shared.discountFor(skill.price) {
  82. discountView.isHidden = false
  83. newbieView.update(1 - discount)
  84. discountCoinLabel.text = "-\(skill.price * (1 - discount))"
  85. } else {
  86. discountView.isHidden = true
  87. }
  88. skillId = skill.id
  89. price = skill.price
  90. curCount = 1
  91. }
  92. func update(_ detail: LNQRCodeDetailResponse, count: Int, extra: String) {
  93. gameNameLabel.text = detail.bizCategoryName
  94. avatar.showAvatar(detail.avatar)
  95. nameLabel.text = detail.nickname
  96. priceLabel.text = "\(detail.price.toDisplay)/\(detail.unit)"
  97. if let discount = LNOrderManager.shared.discountFor(detail.price) {
  98. discountView.isHidden = false
  99. newbieView.update(1 - discount)
  100. discountCoinLabel.text = "-\(detail.price * (1 - discount))"
  101. } else {
  102. discountView.isHidden = true
  103. }
  104. qrCode = detail.qrCode
  105. price = detail.price
  106. curCount = count
  107. extraInput.text = extra
  108. }
  109. required init?(coder: NSCoder) {
  110. fatalError("init(coder:) has not been implemented")
  111. }
  112. }
  113. extension LNCreateOrderPanel: LNPurchaseManagerNotify {
  114. func onUserWalletInfoChanged(info: LNUserWalletInfo) {
  115. curCoinLabel.text = info.coin.toDisplay
  116. }
  117. }
  118. extension LNCreateOrderPanel: UITextFieldDelegate {
  119. func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  120. let currentText = textField.text ?? ""
  121. guard let range = Range(range, in: currentText) else { return false }
  122. let newText = currentText.replacingCharacters(in: range, with: string)
  123. if newText.count < currentText.count {
  124. return true
  125. }
  126. return newText.count <= LNOrderManager.orderExtraMaxLength
  127. }
  128. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  129. textField.resignFirstResponder()
  130. return true
  131. }
  132. }
  133. extension LNCreateOrderPanel {
  134. private func setupViews() {
  135. container.backgroundColor = .primary_1
  136. let stackView = UIStackView()
  137. stackView.axis = .vertical
  138. stackView.spacing = 10
  139. container.addSubview(stackView)
  140. stackView.snp.makeConstraints { make in
  141. make.horizontalEdges.equalToSuperview().inset(12)
  142. make.top.equalToSuperview().offset(20)
  143. }
  144. let billInfo = buildBillInfo()
  145. stackView.addArrangedSubview(billInfo)
  146. let extra = buildExtra()
  147. stackView.addArrangedSubview(extra)
  148. let wallet = buildWalletInfo()
  149. stackView.addArrangedSubview(wallet)
  150. let confirmButton = UIButton()
  151. confirmButton.setTitle(.init(key: "A00123"), for: .normal)
  152. confirmButton.setTitleColor(.text_1, for: .normal)
  153. confirmButton.titleLabel?.font = .heading_h3
  154. confirmButton.setBackgroundImage(.primary_8, for: .normal)
  155. confirmButton.layer.cornerRadius = 23.5
  156. confirmButton.clipsToBounds = true
  157. confirmButton.addAction(UIAction(handler: { [weak self] _ in
  158. guard let self else { return }
  159. let extra = extraInput.text ?? ""
  160. if let skillId {
  161. LNOrderManager.shared.createOrder(
  162. skillId: skillId, count: curCount, remark: extra)
  163. { [weak self] orderNo in
  164. guard let self else { return }
  165. guard let orderNo else { return }
  166. dismiss()
  167. completionHandler?(orderNo)
  168. }
  169. } else if let qrCode {
  170. LNOrderManager.shared.createQRCodeOrder(
  171. data: qrCode, count: curCount, extra: extra)
  172. { [weak self] orderNo in
  173. guard let self else { return }
  174. guard let orderNo else { return }
  175. dismiss()
  176. completionHandler?(orderNo)
  177. }
  178. }
  179. }), for: .touchUpInside)
  180. container.addSubview(confirmButton)
  181. confirmButton.snp.makeConstraints { make in
  182. make.horizontalEdges.equalToSuperview().inset(12)
  183. make.top.equalTo(stackView.snp.bottom).offset(15)
  184. make.bottom.equalToSuperview().offset(commonBottomInset)
  185. make.height.equalTo(47)
  186. }
  187. }
  188. private func buildBillInfo() -> UIView {
  189. let container = UIView()
  190. container.backgroundColor = .fill
  191. container.layer.cornerRadius = 12
  192. container.onTap { [weak self] in
  193. guard let self else { return }
  194. endEditing(true)
  195. }
  196. let stackView = UIStackView()
  197. stackView.axis = .vertical
  198. container.addSubview(stackView)
  199. stackView.snp.makeConstraints { make in
  200. make.edges.equalToSuperview()
  201. }
  202. stackView.addArrangedSubview(buildUserInfo())
  203. stackView.addArrangedSubview(buildPrice())
  204. stackView.addArrangedSubview(buildDiscount())
  205. stackView.addArrangedSubview(buildCost())
  206. return container
  207. }
  208. private func buildUserInfo() -> UIView {
  209. let container = UIView()
  210. container.isUserInteractionEnabled = false
  211. container.snp.makeConstraints { make in
  212. make.height.equalTo(83)
  213. }
  214. let skillTag = UIImageView()
  215. skillTag.image = .icOrderSkillBg
  216. container.addSubview(skillTag)
  217. skillTag.snp.makeConstraints { make in
  218. make.leading.top.equalToSuperview()
  219. }
  220. avatar.layer.cornerRadius = 30
  221. avatar.layer.borderWidth = 2
  222. avatar.layer.borderColor = UIColor.fill.cgColor
  223. avatar.backgroundColor = .fill
  224. avatar.clipsToBounds = true
  225. container.addSubview(avatar)
  226. avatar.snp.makeConstraints { make in
  227. make.centerX.equalToSuperview()
  228. make.top.equalToSuperview().offset(-8)
  229. make.width.height.equalTo(60)
  230. }
  231. gameNameLabel.font = .heading_h5
  232. gameNameLabel.textColor = .text_1
  233. gameNameLabel.textAlignment = .center
  234. skillTag.addSubview(gameNameLabel)
  235. gameNameLabel.snp.makeConstraints { make in
  236. make.centerY.equalToSuperview()
  237. make.horizontalEdges.equalToSuperview().inset(14)
  238. make.width.lessThanOrEqualTo(82)
  239. }
  240. nameLabel.font = .heading_h4
  241. nameLabel.textColor = .text_5
  242. container.addSubview(nameLabel)
  243. nameLabel.snp.makeConstraints { make in
  244. make.centerX.equalToSuperview()
  245. make.top.equalTo(avatar.snp.bottom).offset(3)
  246. }
  247. let line = UIView()
  248. line.backgroundColor = .fill_2
  249. container.addSubview(line)
  250. line.snp.makeConstraints { make in
  251. make.horizontalEdges.equalToSuperview().inset(12)
  252. make.height.equalTo(1)
  253. make.bottom.equalToSuperview()
  254. }
  255. return container
  256. }
  257. private func buildPrice() -> UIView {
  258. let container = UIView()
  259. container.snp.makeConstraints { make in
  260. make.height.equalTo(49)
  261. }
  262. let titleLabel = UILabel()
  263. titleLabel.font = .heading_h5
  264. titleLabel.textColor = .text_5
  265. titleLabel.text = .init(key: "A00128")
  266. container.addSubview(titleLabel)
  267. titleLabel.snp.makeConstraints { make in
  268. make.leading.equalToSuperview().offset(12)
  269. make.centerY.equalToSuperview()
  270. }
  271. let coin = UIImageView.coinImageView()
  272. container.addSubview(coin)
  273. coin.snp.makeConstraints { make in
  274. make.centerY.equalToSuperview()
  275. make.leading.equalTo(titleLabel.snp.trailing).offset(6)
  276. make.width.height.equalTo(18)
  277. }
  278. priceLabel.font = .body_m
  279. priceLabel.textColor = .text_5
  280. container.addSubview(priceLabel)
  281. priceLabel.snp.makeConstraints { make in
  282. make.leading.equalTo(coin.snp.trailing).offset(2)
  283. make.centerY.equalToSuperview()
  284. }
  285. countLabel.font = .body_m
  286. countLabel.textColor = .text_5
  287. container.addSubview(countLabel)
  288. countLabel.snp.makeConstraints { make in
  289. make.centerY.equalToSuperview()
  290. make.trailing.equalToSuperview().offset(-16)
  291. }
  292. countEditView.isHidden = true
  293. container.addSubview(countEditView)
  294. countEditView.snp.makeConstraints { make in
  295. make.verticalEdges.equalToSuperview()
  296. make.trailing.equalToSuperview()
  297. }
  298. addButton.setTitle("+", for: .normal)
  299. addButton.setTitleColor(.text_4, for: .normal)
  300. addButton.setTitleColor(.text_2, for: .disabled)
  301. addButton.backgroundColor = .primary_1
  302. addButton.layer.cornerRadius = 12
  303. addButton.addAction(UIAction(handler: { [weak self] _ in
  304. guard let self else { return }
  305. curCount += 1
  306. }), for: .touchUpInside)
  307. countEditView.addSubview(addButton)
  308. addButton.snp.makeConstraints { make in
  309. make.centerY.equalToSuperview()
  310. make.trailing.equalToSuperview().offset(-12)
  311. make.width.height.equalTo(24)
  312. }
  313. customCountLabel.font = .body_m
  314. customCountLabel.textColor = .text_5
  315. countEditView.addSubview(customCountLabel)
  316. customCountLabel.snp.makeConstraints { make in
  317. make.centerY.equalToSuperview()
  318. make.trailing.equalTo(addButton.snp.leading).offset(-10)
  319. }
  320. minuButton.setTitle("-", for: .normal)
  321. minuButton.setTitleColor(.text_4, for: .normal)
  322. minuButton.setTitleColor(.text_2, for: .disabled)
  323. minuButton.backgroundColor = .primary_1
  324. minuButton.layer.cornerRadius = 12
  325. minuButton.addAction(UIAction(handler: { [weak self] _ in
  326. guard let self else { return }
  327. self.curCount -= 1
  328. }), for: .touchUpInside)
  329. countEditView.addSubview(minuButton)
  330. minuButton.snp.makeConstraints { make in
  331. make.centerY.equalToSuperview()
  332. make.trailing.equalTo(customCountLabel.snp.leading).offset(-10)
  333. make.width.height.equalTo(24)
  334. make.leading.equalToSuperview()
  335. }
  336. return container
  337. }
  338. private func buildDiscount() -> UIView {
  339. discountView.snp.makeConstraints { make in
  340. make.height.equalTo(49)
  341. }
  342. let titleLabel = UILabel()
  343. titleLabel.font = .heading_h5
  344. titleLabel.textColor = .text_5
  345. titleLabel.text = .init(key: "A00129")
  346. discountView.addSubview(titleLabel)
  347. titleLabel.snp.makeConstraints { make in
  348. make.leading.equalToSuperview().offset(12)
  349. make.centerY.equalToSuperview()
  350. }
  351. discountView.addSubview(newbieView)
  352. newbieView.snp.makeConstraints { make in
  353. make.centerY.equalToSuperview()
  354. make.leading.equalTo(titleLabel.snp.trailing).offset(8)
  355. }
  356. discountCoinLabel.font = .heading_h3
  357. discountCoinLabel.textColor = .init(hex: "#FF6F32")
  358. discountView.addSubview(discountCoinLabel)
  359. discountCoinLabel.snp.makeConstraints { make in
  360. make.centerY.equalToSuperview()
  361. make.trailing.equalToSuperview().offset(-12)
  362. }
  363. let coin = UIImageView.coinImageView()
  364. discountView.addSubview(coin)
  365. coin.snp.makeConstraints { make in
  366. make.centerY.equalToSuperview()
  367. make.trailing.equalTo(discountCoinLabel.snp.leading).offset(-2)
  368. make.width.height.equalTo(20)
  369. }
  370. return discountView
  371. }
  372. private func buildCost() -> UIView {
  373. let container = UIView()
  374. container.isUserInteractionEnabled = false
  375. container.snp.makeConstraints { make in
  376. make.height.equalTo(49)
  377. }
  378. costLabel.font = .heading_h1
  379. costLabel.textColor = .text_5
  380. container.addSubview(costLabel)
  381. costLabel.snp.makeConstraints { make in
  382. make.centerY.equalToSuperview()
  383. make.trailing.equalToSuperview().offset(-12)
  384. }
  385. let coin = UIImageView.coinImageView()
  386. container.addSubview(coin)
  387. coin.snp.makeConstraints { make in
  388. make.centerY.equalToSuperview()
  389. make.trailing.equalTo(costLabel.snp.leading).offset(-2)
  390. make.width.height.equalTo(24)
  391. }
  392. let totalLabel = UILabel()
  393. totalLabel.text = .init(key: "A00124")
  394. totalLabel.font = .body_m
  395. totalLabel.textColor = .text_5
  396. container.addSubview(totalLabel)
  397. totalLabel.snp.makeConstraints { make in
  398. make.centerY.equalToSuperview()
  399. make.trailing.equalTo(coin.snp.leading).offset(-2)
  400. }
  401. return container
  402. }
  403. private func buildExtra() -> UIView {
  404. extraView.isHidden = true
  405. extraView.backgroundColor = .fill
  406. extraView.layer.cornerRadius = 12
  407. extraView.snp.makeConstraints { make in
  408. make.height.equalTo(50)
  409. }
  410. let titleLabel = UILabel()
  411. titleLabel.font = .heading_h5
  412. titleLabel.text = .init(key: "A00086")
  413. titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  414. titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  415. titleLabel.textColor = .text_5
  416. extraView.addSubview(titleLabel)
  417. titleLabel.snp.makeConstraints { make in
  418. make.leading.equalToSuperview().offset(12)
  419. make.centerY.equalToSuperview()
  420. }
  421. extraInput.placeholder = .init(key: "A00125", LNOrderManager.orderExtraMaxLength)
  422. extraInput.font = .body_s
  423. extraInput.textColor = .text_5
  424. extraInput.clearButtonMode = .whileEditing
  425. extraInput.delegate = self
  426. extraInput.visibleView = extraView
  427. extraView.addSubview(extraInput)
  428. extraInput.snp.makeConstraints { make in
  429. make.centerY.equalToSuperview()
  430. make.leading.equalTo(titleLabel.snp.trailing).offset(8)
  431. make.trailing.equalToSuperview().offset(-12)
  432. }
  433. return extraView
  434. }
  435. private func buildWalletInfo() -> UIView {
  436. let container = UIView()
  437. container.isUserInteractionEnabled = false
  438. container.backgroundColor = .fill
  439. container.layer.cornerRadius = 12
  440. container.snp.makeConstraints { make in
  441. make.height.equalTo(66)
  442. }
  443. let icon = UIImageView()
  444. icon.image = .icWalletWithBg
  445. container.addSubview(icon)
  446. icon.snp.makeConstraints { make in
  447. make.centerY.equalToSuperview()
  448. make.leading.equalToSuperview().offset(12)
  449. }
  450. let infoView = UIView()
  451. container.addSubview(infoView)
  452. infoView.snp.makeConstraints { make in
  453. make.centerY.equalToSuperview()
  454. make.leading.equalTo(icon.snp.trailing).offset(6)
  455. }
  456. let titleLabel = UILabel()
  457. titleLabel.text = .init(key: "A00126")
  458. titleLabel.font = .heading_h5
  459. titleLabel.textColor = .text_5
  460. infoView.addSubview(titleLabel)
  461. titleLabel.snp.makeConstraints { make in
  462. make.leading.top.equalToSuperview()
  463. make.trailing.equalToSuperview()
  464. }
  465. curCoinLabel.text = myWalletInfo.coin.toDisplay
  466. curCoinLabel.font = .body_m
  467. curCoinLabel.textColor = .text_3
  468. infoView.addSubview(curCoinLabel)
  469. curCoinLabel.snp.makeConstraints { make in
  470. make.trailing.bottom.equalToSuperview()
  471. make.top.equalTo(titleLabel.snp.bottom).offset(2)
  472. }
  473. let coin = UIImageView.coinImageView()
  474. infoView.addSubview(coin)
  475. coin.snp.makeConstraints { make in
  476. make.centerY.equalTo(curCoinLabel)
  477. make.leading.equalToSuperview()
  478. make.trailing.equalTo(curCoinLabel.snp.leading).offset(-2)
  479. make.width.height.equalTo(16)
  480. }
  481. let check = UIImageView()
  482. check.image = .icCheck
  483. container.addSubview(check)
  484. check.snp.makeConstraints { make in
  485. make.centerY.equalToSuperview()
  486. make.trailing.equalToSuperview().offset(-12)
  487. make.width.height.equalTo(22)
  488. }
  489. return container
  490. }
  491. }
  492. #if DEBUG
  493. import SwiftUI
  494. struct LNCreateOrderPanelPreview: UIViewRepresentable {
  495. func makeUIView(context: Context) -> some UIView {
  496. let container = UIView()
  497. container.backgroundColor = .lightGray
  498. let view = LNCreateOrderPanel()
  499. view.popup(container)
  500. return container
  501. }
  502. func updateUIView(_ uiView: UIViewType, context: Context) { }
  503. }
  504. #Preview(body: {
  505. LNCreateOrderPanelPreview()
  506. })
  507. #endif // DEBUG