// // LNOrderDetailViewController.swift // Lanu // // Created by OneeChan on 2025/11/19. // import Foundation import UIKit import SnapKit extension UIView { func pushToOrderDetail(orderId: String) { let vc = LNOrderDetailViewController(orderId: orderId) navigationController?.pushViewController(vc, animated: true) } } class LNOrderDetailViewController: LNViewController { private let orderId: String private let backgroundIc = UIImageView() private let starView = LNFiveStarScoreView() private let stateIc = UIImageView() private let stateLabel = UILabel() private var timer: Timer? private let countDownLabel = UILabel() private let detailView = LNOrderDetailCardView() private let cancelButton = UIButton() private let refundButton = UIButton() private let commentButton = UIButton() private let completeView = UIView() private let refundView = LNOrderRefundInfoView() private var curDetail: LNOrderDetailResponse? init(orderId: String) { self.orderId = orderId super.init(nibName: nil, bundle: nil) } override func viewDidLoad() { super.viewDidLoad() setupViews() LNEventDeliver.addObserver(self) reloadDetail() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNOrderDetailViewController: LNOrderManagerNotify { func onOrderInfoChanged(orderId: String) { guard orderId == self.orderId else { return } reloadDetail() } } extension LNOrderDetailViewController { private func reloadDetail() { LNOrderManager.shared.getOrderDetail(orderId: orderId) { [weak self] detail in guard let self else { return } guard let detail else { return } curDetail = detail update(detail) } } } extension LNOrderDetailViewController { private func startCountDown() { stopCountDown() updateRemain() let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self else { return } if updateRemain() { stopCountDown() reloadDetail() } } RunLoop.current.add(timer, forMode: .common) self.timer = timer } private func stopCountDown() { timer?.invalidate() timer = nil } @discardableResult private func updateRemain() -> Bool { guard let curDetail else { return true } let remain = 3600 - (Int(curTime) - curDetail.orderInfo.createTime / 1_000) let countDownText = remain.timeCountDisplay let text: String = .init(key: "A00134", countDownText) let attr = NSMutableAttributedString(string: text) let range = (text as NSString).range(of: countDownText) attr.addAttributes([.font: UIFont.heading_h4, .foregroundColor: UIColor.primary_5], range: range) countDownLabel.attributedText = attr return remain <= 0 } } extension LNOrderDetailViewController { private func update(_ item: LNOrderDetailResponse) { starView.isHidden = true countDownLabel.isHidden = true cancelButton.isHidden = true commentButton.isHidden = true completeView.isHidden = true refundButton.isHidden = true stopCountDown() detailView.update(item) refundView.update(item) if item.orderInfo.refundApply { backgroundIc.image = .icOrderCancelledBg stateIc.image = .icOrderStatusInProgress stateLabel.text = .init(key: "A00135") return } switch item.orderInfo.status { case .created: break case .waitingForAccept: backgroundIc.image = .icOrderNormalBg stateIc.image = .icOrderStatusInProgress stateLabel.text = .init(key: "A00136") countDownLabel.isHidden = false cancelButton.isHidden = false startCountDown() case .completed: backgroundIc.image = .icOrderNormalBg stateIc.image = .icOrderStatusComplete stateLabel.text = .init(key: "A00137") starView.isHidden = item.orderInfo.star == 0 starView.score = item.orderInfo.star commentButton.isHidden = item.orderInfo.star != 0 case .refunded: backgroundIc.image = .icOrderCancelledBg stateIc.image = .icOrderStatusRefund stateLabel.text = .init(key: "A00138") case .accepted: backgroundIc.image = .icOrderNormalBg stateIc.image = .icOrderStatusWaiting stateLabel.text = .init(key: "A00139") refundButton.isHidden = false case .rejected: backgroundIc.image = .icOrderCancelledBg stateIc.image = .icOrderStatusCancel stateLabel.text = .init(key: "A00140") case .servicing: backgroundIc.image = .icOrderNormalBg stateIc.image = .icOrderStatusInProgress stateLabel.text = .init(key: "A00141") refundButton.isHidden = false case .serviceDone: backgroundIc.image = .icOrderNormalBg stateIc.image = .icOrderStatusInProgress stateLabel.text = .init(key: "A00141") completeView.isHidden = false case .cancelled: backgroundIc.image = .icOrderCancelledBg stateIc.image = .icOrderStatusCancel stateLabel.text = .init(key: "A00152") } } private func setupViews() { view.backgroundColor = .primary_1 title = .init(key: "A00143") view.addSubview(backgroundIc) backgroundIc.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() } let bottomMenu = buildBottomMenu() view.addSubview(bottomMenu) bottomMenu.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.bottom.equalToSuperview().offset(-view.safeBottomInset - 10) make.height.equalTo(0).priority(.low) } let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false view.addSubview(scrollView) scrollView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() make.bottom.equalTo(bottomMenu.snp.top) } let fakeView = UIView() scrollView.addSubview(fakeView) fakeView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalToSuperview() make.width.equalToSuperview() make.height.equalTo(0) } let statusView = buildStatusView() scrollView.addSubview(statusView) statusView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(24) make.top.equalToSuperview().offset(44) } let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 10 scrollView.addSubview(stackView) stackView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(16) make.bottom.equalToSuperview() make.top.equalTo(statusView.snp.bottom).offset(30) } stackView.addArrangedSubview(detailView) refundView.isHidden = true stackView.addArrangedSubview(refundView) } private func buildStatusView() -> UIView { let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 7 stackView.alignment = .center starView.icSize = 30 starView.spacing = 14 starView.startType = .whiteBorder stackView.addArrangedSubview(starView) let stateView = UIView() stateView.addSubview(stateIc) stateIc.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview() make.width.height.equalTo(24) } stateLabel.font = .heading_h1 stateLabel.textColor = .text_5 stateLabel.numberOfLines = 0 stateView.addSubview(stateLabel) stateLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.trailing.equalToSuperview() make.leading.equalTo(stateIc.snp.trailing).offset(8) } stackView.addArrangedSubview(stateView) countDownLabel.font = .body_s countDownLabel.textColor = .text_5 stackView.addArrangedSubview(countDownLabel) return stackView } private func buildBottomMenu() -> UIView { let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 0 stackView.snp.makeConstraints { make in make.height.equalTo(0).priority(.low) } cancelButton.isHidden = true cancelButton.setTitle(.init(key: "A00003"), for: .normal) cancelButton.setTitleColor(.text_3, for: .normal) cancelButton.titleLabel?.font = .heading_h3 cancelButton.layer.cornerRadius = 23.5 cancelButton.layer.borderWidth = 1 cancelButton.layer.borderColor = UIColor.text_2.cgColor cancelButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } LNCommonAlertView.showCancelOrderAlert(orderId: orderId) }), for: .touchUpInside) stackView.addArrangedSubview(cancelButton) cancelButton.snp.makeConstraints { make in make.height.equalTo(47) } refundButton.isHidden = true refundButton.setTitle(.init(key: "A00144"), for: .normal) refundButton.setTitleColor(.text_3, for: .normal) refundButton.titleLabel?.font = .heading_h3 refundButton.layer.cornerRadius = 23.5 refundButton.layer.borderWidth = 1 refundButton.layer.borderColor = UIColor.text_2.cgColor refundButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } view.pushToOrderRefund(orderId) }), for: .touchUpInside) stackView.addArrangedSubview(refundButton) refundButton.snp.makeConstraints { make in make.height.equalTo(47) } let cover = UIView() cover.backgroundColor = .fill cover.layer.cornerRadius = 23 cover.isUserInteractionEnabled = false commentButton.addSubview(cover) cover.snp.makeConstraints { make in make.edges.equalToSuperview().inset(1) } commentButton.isHidden = true commentButton.setBackgroundImage(.primary_7, for: .normal) commentButton.setTitle(.init(key: "A00145"), for: .normal) commentButton.setTitleColor(.text_6, for: .normal) commentButton.titleLabel?.font = .heading_h3 commentButton.layer.cornerRadius = 23.5 commentButton.clipsToBounds = true commentButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } guard let curDetail else { return } let panel = LNOrderCommentPanel() panel.update(curDetail.orderInfo.avatar, orderId: orderId) panel.popup() }), for: .touchUpInside) stackView.addArrangedSubview(commentButton) commentButton.snp.makeConstraints { make in make.height.equalTo(47) } completeView.isHidden = true stackView.addArrangedSubview(completeView) completeView.snp.makeConstraints { make in make.height.equalTo(47) } let refundButton = UIButton() refundButton.layer.cornerRadius = 23.5 refundButton.setTitle(.init(key: "A00144"), for: .normal) refundButton.setTitleColor(.text_3, for: .normal) refundButton.titleLabel?.font = .heading_h3 refundButton.layer.borderColor = UIColor.text_2.cgColor refundButton.layer.borderWidth = 1 refundButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } view.pushToOrderRefund(orderId) }), for: .touchUpInside) completeView.addSubview(refundButton) refundButton.snp.makeConstraints { make in make.leading.equalToSuperview() make.verticalEdges.equalToSuperview() make.width.equalToSuperview().multipliedBy(1.0/3.0) } let completeButton = UIButton() completeButton.layer.cornerRadius = 23.5 completeButton.setTitle(.init(key: "A00025"), for: .normal) completeButton.setTitleColor(.text_1, for: .normal) completeButton.titleLabel?.font = .heading_h3 completeButton.layer.cornerRadius = 23.5 completeButton.setBackgroundImage(.primary_8, for: .normal) completeButton.clipsToBounds = true completeButton.addAction(UIAction(handler: { [weak self] _ in guard let self else { return } LNCommonAlertView.showFinishOrderAlert(orderId: orderId, completion: nil) }), for: .touchUpInside) completeView.addSubview(completeButton) completeButton.snp.makeConstraints { make in make.verticalEdges.equalToSuperview() make.leading.equalTo(refundButton.snp.trailing).offset(10) make.trailing.equalToSuperview() } return stackView } } #if DEBUG import SwiftUI struct LNOrderDetailViewControllerPreview: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { LNOrderDetailViewController(orderId: "") } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } } #Preview(body: { LNOrderDetailViewControllerPreview() }) #endif