// // LNCreateOrderViewController.swift // Lanu // // Created by OneeChan on 2025/12/22. // import Foundation import UIKit import SnapKit import Combine extension UIView { func pushToCreateOrder(_ skillDetail: LNGameMateSkillDetailVO, count: Int = 1) { let vc = LNCreateOrderViewController() vc.loadSkill(skillDetail, count: count) navigationController?.pushViewController(vc, animated: true) } func pushToCreateOrder(_ qrCode: LNQRCodeDetailResponse) { let vc = LNCreateOrderViewController() vc.loadQRData(qrCode) navigationController?.pushViewController(vc, animated: true) } } class LNCreateOrderViewController: LNViewController { private var skill: LNGameMateSkillDetailVO? private var qrDetail: LNQRCodeDetailResponse? private let avatar = UIImageView() private let nameLabel = UILabel() private let starLabel = UILabel() private let skillIc = UIImageView() private let skillNameLabel = UILabel() private let skillPriceLabel = UILabel() private let skillUnitLabel = UILabel() private let countLabel = UILabel() private let minuButton = UIButton() private let addButton = UIButton() private var curCount = 1 { didSet { countLabel.text = "\(curCount)" if curCount <= 1 { minuButton.isEnabled = false } else { minuButton.isEnabled = true } updateCost() } } private let extraInput = UITextField() private let costLabel = UILabel() private let orderButton = UIButton() private let emptyDiscountView = UIView() private let newbieDiscountView = UIView() private let discountView = LNNewbieDiscountView() private let discountCoinLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() setupViews() } func loadSkill(_ detail: LNGameMateSkillDetailVO, count: Int) { skill = detail updateContent(detail) curCount = count } func loadQRData(_ data: LNQRCodeDetailResponse) { qrDetail = data updateContent(data) } } extension LNCreateOrderViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let currentText = textField.text ?? "" guard let range = Range(range, in: currentText) else { return false } let newText = currentText.replacingCharacters(in: range, with: string) if newText.count < currentText.count { return true } return newText.count <= LNOrderManager.orderExtraMaxLength } func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } } extension LNCreateOrderViewController { private func updateContent(_ detail: LNGameMateSkillDetailVO) { avatar.showAvatar(detail.avatar) nameLabel.text = detail.nickname starLabel.text = "\(detail.star)" let skill = LNGameMateManager.shared.gameCategory(for: detail.categoryCode) skillIc.sd_setImage(with: URL(string: skill?.icon ?? "")) skillNameLabel.text = detail.categoryName skillPriceLabel.text = "\(detail.price.toDisplay)" skillUnitLabel.text = "/\(detail.unit)" curCount = 1 if let discount = LNOrderManager.shared.discountFor(detail.price) { emptyDiscountView.isHidden = true newbieDiscountView.isHidden = false discountView.update(1 - discount) discountCoinLabel.text = "-\((1 - discount) * detail.price)" } else { emptyDiscountView.isHidden = false newbieDiscountView.isHidden = true } orderButton.isEnabled = true } private func updateContent(_ detail: LNQRCodeDetailResponse) { avatar.showAvatar(detail.avatar) nameLabel.text = detail.nickname starLabel.text = "\(detail.star)" skillIc.sd_setImage(with: URL(string: detail.bizCategoryIcon)) skillNameLabel.text = detail.bizCategoryName skillPriceLabel.text = "\(detail.price.toDisplay)" skillUnitLabel.text = "/\(detail.unit)" curCount = detail.purchaseQty if detail.type != .normal { minuButton.isEnabled = false addButton.isEnabled = false } if let discount = LNOrderManager.shared.discountFor(detail.price) { emptyDiscountView.isHidden = true newbieDiscountView.isHidden = false discountView.update(1 - discount) discountCoinLabel.text = "-\((1 - discount) * detail.price)" } else { emptyDiscountView.isHidden = false newbieDiscountView.isHidden = true } orderButton.isEnabled = !detail.sellerUserNo.isMyUid } private func updateCost() { let price: Double = if let skill { skill.price } else if let qrDetail { qrDetail.price } else { 0 } let cost = price * Double(curCount) if let discount = LNOrderManager.shared.discountFor(price) { costLabel.text = (cost - (1 - discount) * price).toDisplay } else { costLabel.text = cost.toDisplay } // orderUnitLabel.text = "/\(curCount) \(unit)" } private func setupViews() { title = .init(key: "A00127") view.backgroundColor = .primary_1 view.onTap { [weak self] in guard let self else { return } view.endEditing(true) } let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 10 view.addSubview(stackView) stackView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.top.equalToSuperview().offset(16) } stackView.addArrangedSubview(buildUserInfo()) stackView.addArrangedSubview(buildSkillInfo()) stackView.addArrangedSubview(buildExtra()) let menu = buildMenu() view.addSubview(menu) menu.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.bottom.equalToSuperview() } } private func buildUserInfo() -> UIView { let container = UIView() container.backgroundColor = .fill container.layer.cornerRadius = 12 container.snp.makeConstraints { make in make.height.equalTo(60) } let starView = UIView() container.addSubview(starView) starView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-12) } let star = UIImageView() star.image = .icStarFill starView.addSubview(star) star.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview() make.width.height.equalTo(16) } starLabel.font = .heading_h3 starLabel.textColor = .text_5 starView.addSubview(starLabel) starLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.leading.equalTo(star.snp.trailing).offset(4) make.trailing.equalToSuperview() } avatar.layer.cornerRadius = 20 avatar.clipsToBounds = true container.addSubview(avatar) avatar.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() make.width.height.equalTo(40) } nameLabel.font = .heading_h4 nameLabel.textColor = .text_5 container.addSubview(nameLabel) nameLabel.snp.makeConstraints { make in make.leading.equalTo(avatar.snp.trailing).offset(8) make.centerY.equalToSuperview() make.trailing.lessThanOrEqualTo(starView.snp.leading).offset(-12) } return container } private func buildSkillInfo() -> UIView { let container = UIStackView() container.axis = .vertical container.spacing = 0 container.backgroundColor = .fill container.layer.cornerRadius = 12 container.addArrangedSubview(buildInfoView()) container.addArrangedSubview(buildPriceView()) container.addArrangedSubview(buildCountView()) container.addArrangedSubview(buildDiscount()) let emptyView = UIView() emptyView.snp.makeConstraints { make in make.height.equalTo(10) } container.addArrangedSubview(emptyView) return container } private func buildInfoView() -> UIView { let infoView = UIView() infoView.snp.makeConstraints { make in make.height.equalTo(40) } skillIc.layer.cornerRadius = 13 skillIc.clipsToBounds = true infoView.addSubview(skillIc) skillIc.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() make.width.height.equalTo(26) } skillNameLabel.font = .heading_h5 skillNameLabel.textColor = .text_5 infoView.addSubview(skillNameLabel) skillNameLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(skillIc.snp.trailing).offset(2) } let line = UIView() line.backgroundColor = .fill_2 infoView.addSubview(line) line.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(11) make.bottom.equalToSuperview() make.height.equalTo(1) } return infoView } private func buildPriceView() -> UIView { let priceView = UIView() priceView.snp.makeConstraints { make in make.height.equalTo(38) } let priceTitleLabel = UILabel() priceTitleLabel.font = .heading_h5 priceTitleLabel.textColor = .text_5 priceTitleLabel.text = .init(key: "A00128") priceView.addSubview(priceTitleLabel) priceTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() } skillUnitLabel.font = .body_s skillUnitLabel.textColor = .text_4 priceView.addSubview(skillUnitLabel) skillUnitLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-12) } skillPriceLabel.font = .heading_h3 skillPriceLabel.textColor = .text_5 priceView.addSubview(skillPriceLabel) skillPriceLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(skillUnitLabel.snp.leading) } let coinIc = UIImageView.coinImageView() priceView.addSubview(coinIc) coinIc.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(skillPriceLabel.snp.leading).offset(-2) make.width.height.equalTo(20) } return priceView } private func buildCountView() -> UIView { let countView = UIView() countView.snp.makeConstraints { make in make.height.equalTo(38) } let countTitleLabel = UILabel() countTitleLabel.font = .heading_h5 countTitleLabel.textColor = .text_5 countTitleLabel.text = .init(key: "A00122") countView.addSubview(countTitleLabel) countTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() } addButton.setTitle("+", for: .normal) addButton.setTitleColor(.text_4, for: .normal) addButton.setTitleColor(.text_2, for: .disabled) addButton.backgroundColor = .primary_1 addButton.layer.cornerRadius = 12 addButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } curCount += 1 }), for: .touchUpInside) countView.addSubview(addButton) addButton.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-12) make.width.height.equalTo(24) } countLabel.font = .body_m countLabel.textColor = .text_5 countView.addSubview(countLabel) countLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(addButton.snp.leading).offset(-10) } minuButton.setTitle("-", for: .normal) minuButton.setTitleColor(.text_4, for: .normal) minuButton.setTitleColor(.text_2, for: .disabled) minuButton.backgroundColor = .primary_1 minuButton.layer.cornerRadius = 12 minuButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } self.curCount -= 1 }), for: .touchUpInside) countView.addSubview(minuButton) minuButton.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(countLabel.snp.leading).offset(-10) make.width.height.equalTo(24) } return countView } private func buildDiscount() -> UIView { let discountView = UIView() discountView.snp.makeConstraints { make in make.height.equalTo(38) } let discountTitleLabel = UILabel() discountTitleLabel.font = .heading_h5 discountTitleLabel.textColor = .text_5 discountTitleLabel.text = .init(key: "A00129") discountView.addSubview(discountTitleLabel) discountTitleLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() } emptyDiscountView.isHidden = true discountView.addSubview(emptyDiscountView) emptyDiscountView.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.trailing.equalToSuperview() } let discountArrow = UIImageView.arrowImageView(size: 14) discountArrow.tintColor = .text_2 emptyDiscountView.addSubview(discountArrow) discountArrow.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-12) } let discountDescLabel = UILabel() discountDescLabel.font = .body_s discountDescLabel.text = .init(key: "A00130") discountDescLabel.textColor = .text_2 emptyDiscountView.addSubview(discountDescLabel) discountDescLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(discountArrow.snp.leading).offset(-3) make.leading.equalToSuperview() } let newbieView = buildNewbieDiscountView() discountView.addSubview(newbieView) newbieView.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.trailing.equalToSuperview() } return discountView } private func buildNewbieDiscountView() -> UIView { discountCoinLabel.font = .heading_h3 discountCoinLabel.textColor = .init(hex: "#FF6F32") newbieDiscountView.addSubview(discountCoinLabel) discountCoinLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().offset(-10) } let coin = UIImageView(image: .icCoin) newbieDiscountView.addSubview(coin) coin.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(discountCoinLabel.snp.leading).offset(-2) make.width.height.equalTo(20) } newbieDiscountView.addSubview(discountView) discountView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalTo(coin.snp.leading).offset(-4) } return newbieDiscountView } private func buildExtra() -> UIView { let container = UIView() container.backgroundColor = .fill container.layer.cornerRadius = 12 container.snp.makeConstraints { make in make.height.equalTo(50) } let titleLabel = UILabel() titleLabel.font = .heading_h5 titleLabel.textColor = .text_5 titleLabel.text = .init(key: "A00086") titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) container.addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(12) } extraInput.font = .body_s extraInput.textColor = .text_5 extraInput.placeholder = .init(key: "A00125", LNOrderManager.orderExtraMaxLength) extraInput.clearButtonMode = .whileEditing extraInput.delegate = self container.addSubview(extraInput) extraInput.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalTo(titleLabel.snp.trailing).offset(8) make.trailing.equalToSuperview().offset(-12) } return container } private func buildMenu() -> UIView { let holder = UIView() let bottomGradient = CAGradientLayer() bottomGradient.colors = [ UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor, UIColor.white.cgColor ] bottomGradient.locations = [0, 0.5, 1] bottomGradient.startPoint = .init(x: 0, y: 0) bottomGradient.endPoint = .init(x: 0, y: 1) holder.layer.addSublayer(bottomGradient) holder.publisher(for: \.bounds).removeDuplicates().sink { [weak bottomGradient] newValue in guard let bottomGradient else { return } bottomGradient.frame = newValue }.store(in: &cancellables) let container = UIView() container.backgroundColor = .init(hex: "#ECF6FF") container.layer.cornerRadius = 23.5 holder.addSubview(container) container.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.top.equalToSuperview().offset(16) make.bottom.equalToSuperview().offset(-view.safeBottomInset - 10) } orderButton.setBackgroundImage(.icSkillOrderLong, for: .normal) orderButton.setTitle(.init(key: "A00041"), for: .normal) orderButton.setTitleColor(.text_1, for: .normal) orderButton.titleLabel?.font = .heading_h3 orderButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } let panel = LNCreateOrderPanel() if let skill { panel.update(skill, count: curCount, extra: extraInput.text ?? "") } else if let qrDetail { panel.update(qrDetail, count: curCount, extra: extraInput.text ?? "") } else { return } panel.completionHandler = { [weak self] orderId in guard let self else { return } navigationController?.viewControllers.removeAll { $0 is LNCreateOrderViewController } } panel.popup() }), for: .touchUpInside) container.addSubview(orderButton) orderButton.snp.makeConstraints { make in make.trailing.equalToSuperview() make.verticalEdges.equalToSuperview() } let descView = UIView() container.addSubview(descView) descView.snp.makeConstraints { make in make.leading.verticalEdges.equalToSuperview() make.trailing.equalTo(orderButton.snp.leading) } let priceView = UIView() descView.addSubview(priceView) priceView.snp.makeConstraints { make in make.center.equalToSuperview() } let coin = UIImageView.coinImageView() priceView.addSubview(coin) coin.snp.makeConstraints { make in make.leading.centerY.equalToSuperview() make.width.height.equalTo(20) } costLabel.font = .heading_h2 costLabel.textColor = .text_5 priceView.addSubview(costLabel) costLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.leading.equalTo(coin.snp.trailing).offset(4) make.trailing.equalToSuperview() } return holder } } #if DEBUG import SwiftUI struct LNCreateOrderViewControllerPreview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { LNCreateOrderViewController() } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } } #Preview(body: { LNCreateOrderViewControllerPreview() }) #endif