653 lines
25 KiB
Swift
653 lines
25 KiB
Swift
//
|
|
// OrderDialogControllers.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
/// 普通订单退款弹窗入口,对齐 Android `OrderRefundDialog`。
|
|
enum OrderRefundAlertController {
|
|
static func present(
|
|
from presenter: UIViewController,
|
|
order: OrderEntity,
|
|
onConfirm: @escaping (OrderRefundRequest) -> Void
|
|
) {
|
|
let fullAmount = order.orderAmount.isEmpty ? order.actualPayAmount : order.orderAmount
|
|
let dialog = OrderRefundDialogView(
|
|
configuration: .photographerOrder(
|
|
orderNumber: order.orderNumber,
|
|
fullRefundAmount: fullAmount
|
|
)
|
|
)
|
|
dialog.onMessage = { [weak presenter] message in
|
|
(presenter as? BaseViewController)?.showToast(message)
|
|
}
|
|
dialog.onConfirm = { payload in
|
|
onConfirm(
|
|
OrderRefundRequest(
|
|
orderNumber: order.orderNumber,
|
|
refundType: payload.refundType,
|
|
refundAmount: payload.refundAmount,
|
|
refundReason: payload.refundReason
|
|
)
|
|
)
|
|
}
|
|
dialog.show(in: presenter.view)
|
|
}
|
|
}
|
|
|
|
/// 店铺/定金订单退款弹窗入口,对齐 Android `DepositOrderRefundDialog`。
|
|
enum DepositOrderRefundAlertController {
|
|
static func present(
|
|
from presenter: UIViewController,
|
|
orderNumber: String,
|
|
amount: String,
|
|
refundType: Int,
|
|
onConfirm: @escaping (Int, String, String) -> Void
|
|
) {
|
|
let dialog = OrderRefundDialogView(
|
|
configuration: .storeOrder(
|
|
orderNumber: orderNumber,
|
|
actualPayAmount: amount,
|
|
initialMode: OrderRefundMode(rawValue: refundType) ?? .full
|
|
)
|
|
)
|
|
dialog.onMessage = { [weak presenter] message in
|
|
(presenter as? BaseViewController)?.showToast(message)
|
|
}
|
|
dialog.onConfirm = { payload in
|
|
onConfirm(payload.refundType, payload.refundAmount, payload.refundReason)
|
|
}
|
|
dialog.show(in: presenter.view)
|
|
}
|
|
}
|
|
|
|
/// 退款方式,和 Android 常量 `1=全额退款、3=部分退款` 保持一致。
|
|
enum OrderRefundMode: Int {
|
|
case full = 1
|
|
case partial = 3
|
|
}
|
|
|
|
/// 退款弹窗校验场景。
|
|
enum OrderRefundDialogKind {
|
|
case photographerOrder
|
|
case storeOrder
|
|
}
|
|
|
|
/// 退款弹窗确认后的结构化结果。
|
|
struct OrderRefundDialogPayload: Equatable {
|
|
let refundType: Int
|
|
let refundAmount: String
|
|
let refundReason: String
|
|
}
|
|
|
|
/// 退款弹窗校验失败信息。
|
|
struct OrderRefundDialogValidationError: Error, Equatable {
|
|
let message: String
|
|
}
|
|
|
|
/// 退款弹窗输入校验与金额格式化逻辑,对齐 Android 两个退款弹窗。
|
|
enum OrderRefundDialogValidation {
|
|
|
|
static func filterMoneyInput(_ text: String, maxLength: Int) -> String {
|
|
var result = ""
|
|
var hasDot = false
|
|
var decimalCount = 0
|
|
|
|
for character in text {
|
|
guard result.count < maxLength else { break }
|
|
if character.isNumber {
|
|
if hasDot {
|
|
guard decimalCount < 2 else { continue }
|
|
decimalCount += 1
|
|
}
|
|
result.append(character)
|
|
} else if character == ".", !hasDot {
|
|
hasDot = true
|
|
result.append(character)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
static func validate(
|
|
kind: OrderRefundDialogKind,
|
|
mode: OrderRefundMode,
|
|
amountText: String,
|
|
reason: String,
|
|
fullRefundAmount: String,
|
|
maxRefundAmount: String
|
|
) -> Result<OrderRefundDialogPayload, OrderRefundDialogValidationError> {
|
|
let trimmedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
switch kind {
|
|
case .photographerOrder:
|
|
return validatePhotographerOrder(
|
|
mode: mode,
|
|
amountText: amountText,
|
|
reason: trimmedReason,
|
|
fullRefundAmount: fullRefundAmount
|
|
)
|
|
case .storeOrder:
|
|
return validateStoreOrder(
|
|
mode: mode,
|
|
amountText: amountText,
|
|
reason: trimmedReason,
|
|
maxRefundAmount: maxRefundAmount
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func validatePhotographerOrder(
|
|
mode: OrderRefundMode,
|
|
amountText: String,
|
|
reason: String,
|
|
fullRefundAmount: String
|
|
) -> Result<OrderRefundDialogPayload, OrderRefundDialogValidationError> {
|
|
guard !reason.isEmpty else {
|
|
return .failure(OrderRefundDialogValidationError(message: "请输入退款原因"))
|
|
}
|
|
guard reason.count <= 255 else {
|
|
return .failure(OrderRefundDialogValidationError(message: "退款原因最多255个字符"))
|
|
}
|
|
if mode == .partial, amountText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
return .failure(OrderRefundDialogValidationError(message: "请输入退款金额"))
|
|
}
|
|
let refundAmount = mode == .full ? fullRefundAmount : amountText
|
|
return .success(
|
|
OrderRefundDialogPayload(
|
|
refundType: mode.rawValue,
|
|
refundAmount: refundAmount,
|
|
refundReason: reason
|
|
)
|
|
)
|
|
}
|
|
|
|
private static func validateStoreOrder(
|
|
mode: OrderRefundMode,
|
|
amountText: String,
|
|
reason: String,
|
|
maxRefundAmount: String
|
|
) -> Result<OrderRefundDialogPayload, OrderRefundDialogValidationError> {
|
|
guard reason.count >= 5 else {
|
|
return .failure(OrderRefundDialogValidationError(message: "退款原因至少5个字符"))
|
|
}
|
|
guard reason.count <= 255 else {
|
|
return .failure(OrderRefundDialogValidationError(message: "退款原因最多255个字符"))
|
|
}
|
|
guard mode == .partial else {
|
|
return .success(
|
|
OrderRefundDialogPayload(
|
|
refundType: mode.rawValue,
|
|
refundAmount: "0",
|
|
refundReason: reason
|
|
)
|
|
)
|
|
}
|
|
let trimmedAmount = amountText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmedAmount.isEmpty else {
|
|
return .failure(OrderRefundDialogValidationError(message: "请输入退款金额"))
|
|
}
|
|
guard let amount = Double(trimmedAmount), amount > 0 else {
|
|
return .failure(OrderRefundDialogValidationError(message: "请输入有效的退款金额"))
|
|
}
|
|
let maxAmount = parseAmount(maxRefundAmount)
|
|
guard maxAmount <= 0 || amount <= maxAmount else {
|
|
return .failure(OrderRefundDialogValidationError(message: "退款金额不能大于实际支付金额"))
|
|
}
|
|
return .success(
|
|
OrderRefundDialogPayload(
|
|
refundType: mode.rawValue,
|
|
refundAmount: String(format: "%.2f", amount),
|
|
refundReason: reason
|
|
)
|
|
)
|
|
}
|
|
|
|
private static func parseAmount(_ text: String) -> Double {
|
|
text
|
|
.replacingOccurrences(of: ",", with: "")
|
|
.replacingOccurrences(of: "¥", with: "")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.toDoubleOrZero
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
var toDoubleOrZero: Double {
|
|
Double(self) ?? 0
|
|
}
|
|
}
|
|
|
|
/// 自定义订单退款弹窗视图,对齐 Android `OrderRefundDialog` / `DepositOrderRefundDialog`。
|
|
final class OrderRefundDialogView: KeyboardAvoidingDialogView, UITextViewDelegate {
|
|
|
|
/// 退款弹窗局部布局尺寸,保持内容紧凑但不牺牲基础点击区域。
|
|
private enum Layout {
|
|
static let sectionGap: CGFloat = AppSpacing.lg
|
|
static let formGap: CGFloat = AppSpacing.md
|
|
static let controlHeight: CGFloat = AppSpacing.minTouchTarget
|
|
static let amountGroupHeight: CGFloat = 67
|
|
static let remarkHeight: CGFloat = 88
|
|
static let actionTop: CGFloat = 14
|
|
static let bottomInset: CGFloat = AppSpacing.md
|
|
}
|
|
|
|
/// 退款弹窗展示与校验配置,用于区分普通订单和店铺/定金订单。
|
|
struct Configuration {
|
|
let kind: OrderRefundDialogKind
|
|
let orderNumber: String
|
|
let fullRefundAmount: String
|
|
let maxRefundAmount: String
|
|
let initialMode: OrderRefundMode
|
|
let remarkPlaceholder: String
|
|
let moneyMaxLength: Int
|
|
|
|
static func photographerOrder(orderNumber: String, fullRefundAmount: String) -> Configuration {
|
|
Configuration(
|
|
kind: .photographerOrder,
|
|
orderNumber: orderNumber,
|
|
fullRefundAmount: fullRefundAmount,
|
|
maxRefundAmount: fullRefundAmount,
|
|
initialMode: .full,
|
|
remarkPlaceholder: "请输入退款原因(必填)",
|
|
moneyMaxLength: 10
|
|
)
|
|
}
|
|
|
|
static func storeOrder(
|
|
orderNumber: String,
|
|
actualPayAmount: String,
|
|
initialMode: OrderRefundMode
|
|
) -> Configuration {
|
|
Configuration(
|
|
kind: .storeOrder,
|
|
orderNumber: orderNumber,
|
|
fullRefundAmount: "0",
|
|
maxRefundAmount: actualPayAmount,
|
|
initialMode: initialMode,
|
|
remarkPlaceholder: "请输入退款原因(必填,5-255字)",
|
|
moneyMaxLength: 15
|
|
)
|
|
}
|
|
}
|
|
|
|
var onConfirm: ((OrderRefundDialogPayload) -> Void)?
|
|
var onMessage: ((String) -> Void)?
|
|
|
|
private let configuration: Configuration
|
|
private var selectedMode: OrderRefundMode
|
|
|
|
private let dimView = UIView()
|
|
private let cardView = UIView()
|
|
private let titleLabel = UILabel()
|
|
private let separatorView = UIView()
|
|
private let orderTitleLabel = UILabel()
|
|
private let orderNumberLabel = UILabel()
|
|
private let refundModeTitleLabel = UILabel()
|
|
private let modeButtonStack = UIStackView()
|
|
private let fullRefundButton = UIButton(type: .system)
|
|
private let partialRefundButton = UIButton(type: .system)
|
|
private let amountGroupView = UIView()
|
|
private let amountTitleLabel = UILabel()
|
|
private let amountContainerView = UIView()
|
|
private let amountField = UITextField()
|
|
private let remarkTitleLabel = UILabel()
|
|
private let remarkContainerView = UIView()
|
|
private let remarkTextView = UITextView()
|
|
private let remarkPlaceholderLabel = UILabel()
|
|
private let actionRow = UIStackView()
|
|
private let cancelButton = UIButton(type: .system)
|
|
private let confirmButton = UIButton(type: .system)
|
|
|
|
init(configuration: Configuration) {
|
|
self.configuration = configuration
|
|
self.selectedMode = configuration.initialMode
|
|
super.init(frame: .zero)
|
|
setupUI()
|
|
applyMode(animated: false)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func show(in parent: UIView) {
|
|
frame = parent.bounds
|
|
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
alpha = 0
|
|
parent.addSubview(self)
|
|
UIView.animate(withDuration: 0.2) {
|
|
self.alpha = 1
|
|
}
|
|
}
|
|
|
|
func dismiss() {
|
|
endEditing(true)
|
|
UIView.animate(withDuration: 0.2, animations: {
|
|
self.alpha = 0
|
|
}, completion: { _ in
|
|
self.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
private func setupUI() {
|
|
keyboardAvoidingContentView = cardView
|
|
keyboardAvoidingMinimumSpacing = AppSpacing.sm
|
|
|
|
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
|
|
dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(cancelTapped)))
|
|
|
|
cardView.backgroundColor = .white
|
|
cardView.layer.cornerRadius = AppRadius.md
|
|
cardView.clipsToBounds = true
|
|
|
|
titleLabel.text = "订单退款"
|
|
titleLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
|
titleLabel.textColor = .black
|
|
|
|
separatorView.backgroundColor = UIColor(hex: 0xF4F4F4)
|
|
|
|
configureSectionLabel(orderTitleLabel, text: "订单号")
|
|
orderNumberLabel.text = configuration.orderNumber
|
|
orderNumberLabel.font = .systemFont(ofSize: 16)
|
|
orderNumberLabel.textColor = .black
|
|
orderNumberLabel.numberOfLines = 0
|
|
|
|
configureSectionLabel(refundModeTitleLabel, text: "退款方式")
|
|
configureModeButton(fullRefundButton, title: "全额退款")
|
|
configureModeButton(partialRefundButton, title: "部分退款")
|
|
fullRefundButton.addTarget(self, action: #selector(fullRefundTapped), for: .touchUpInside)
|
|
partialRefundButton.addTarget(self, action: #selector(partialRefundTapped), for: .touchUpInside)
|
|
|
|
modeButtonStack.axis = .horizontal
|
|
modeButtonStack.spacing = AppSpacing.md
|
|
modeButtonStack.distribution = .fillEqually
|
|
|
|
configureSectionLabel(amountTitleLabel, text: "退款金额")
|
|
amountContainerView.layer.borderWidth = 1
|
|
amountContainerView.layer.borderColor = UIColor(hex: 0xB6BECA).cgColor
|
|
amountContainerView.layer.cornerRadius = AppRadius.sm
|
|
|
|
amountField.placeholder = "请输入退款金额"
|
|
amountField.font = .systemFont(ofSize: 14)
|
|
amountField.textColor = AppColor.textPrimary
|
|
amountField.keyboardType = .decimalPad
|
|
amountField.borderStyle = .none
|
|
amountField.addTarget(self, action: #selector(amountChanged), for: .editingChanged)
|
|
|
|
configureSectionLabel(remarkTitleLabel, text: "退款备注")
|
|
remarkContainerView.layer.borderWidth = 1
|
|
remarkContainerView.layer.borderColor = UIColor(hex: 0xB6BECA).cgColor
|
|
remarkContainerView.layer.cornerRadius = AppRadius.sm
|
|
|
|
remarkTextView.font = .systemFont(ofSize: 14)
|
|
remarkTextView.textColor = AppColor.textPrimary
|
|
remarkTextView.backgroundColor = .clear
|
|
remarkTextView.textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8)
|
|
remarkTextView.delegate = self
|
|
|
|
remarkPlaceholderLabel.text = configuration.remarkPlaceholder
|
|
remarkPlaceholderLabel.font = .systemFont(ofSize: 14)
|
|
remarkPlaceholderLabel.textColor = AppColor.textTertiary
|
|
|
|
actionRow.axis = .horizontal
|
|
actionRow.alignment = .center
|
|
actionRow.spacing = AppSpacing.xs
|
|
|
|
configureActionButton(cancelButton, title: "取消", foreground: AppColor.primary, background: AppColor.primaryLight)
|
|
configureActionButton(confirmButton, title: "确认", foreground: .white, background: AppColor.primary)
|
|
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
|
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
|
|
|
addSubview(dimView)
|
|
addSubview(cardView)
|
|
cardView.addSubview(titleLabel)
|
|
cardView.addSubview(separatorView)
|
|
cardView.addSubview(orderTitleLabel)
|
|
cardView.addSubview(orderNumberLabel)
|
|
cardView.addSubview(refundModeTitleLabel)
|
|
cardView.addSubview(modeButtonStack)
|
|
modeButtonStack.addArrangedSubview(fullRefundButton)
|
|
modeButtonStack.addArrangedSubview(partialRefundButton)
|
|
cardView.addSubview(amountGroupView)
|
|
amountGroupView.addSubview(amountTitleLabel)
|
|
amountGroupView.addSubview(amountContainerView)
|
|
amountContainerView.addSubview(amountField)
|
|
cardView.addSubview(remarkTitleLabel)
|
|
cardView.addSubview(remarkContainerView)
|
|
remarkContainerView.addSubview(remarkTextView)
|
|
remarkContainerView.addSubview(remarkPlaceholderLabel)
|
|
cardView.addSubview(actionRow)
|
|
actionRow.addArrangedSubview(cancelButton)
|
|
actionRow.addArrangedSubview(confirmButton)
|
|
|
|
dimView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
}
|
|
cardView.snp.makeConstraints { make in
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
keyboardAvoidingCenterYConstraint = make.centerY.equalToSuperview().constraint
|
|
}
|
|
titleLabel.snp.makeConstraints { make in
|
|
make.top.equalToSuperview().offset(AppSpacing.sm)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
separatorView.snp.makeConstraints { make in
|
|
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.sm)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
make.height.equalTo(1)
|
|
}
|
|
orderTitleLabel.snp.makeConstraints { make in
|
|
make.top.equalTo(separatorView.snp.bottom).offset(AppSpacing.md)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
orderNumberLabel.snp.makeConstraints { make in
|
|
make.top.equalTo(orderTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
refundModeTitleLabel.snp.makeConstraints { make in
|
|
make.top.equalTo(orderNumberLabel.snp.bottom).offset(Layout.sectionGap)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
modeButtonStack.snp.makeConstraints { make in
|
|
make.top.equalTo(refundModeTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
make.height.equalTo(Layout.controlHeight)
|
|
}
|
|
amountGroupView.snp.makeConstraints { make in
|
|
make.top.equalTo(modeButtonStack.snp.bottom).offset(Layout.formGap)
|
|
make.leading.trailing.equalToSuperview()
|
|
make.height.equalTo(Layout.amountGroupHeight)
|
|
}
|
|
amountTitleLabel.snp.makeConstraints { make in
|
|
make.top.leading.trailing.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: 0, right: AppSpacing.md))
|
|
}
|
|
amountContainerView.snp.makeConstraints { make in
|
|
make.top.equalTo(amountTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
make.height.equalTo(Layout.controlHeight)
|
|
}
|
|
amountField.snp.makeConstraints { make in
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
|
make.centerY.equalToSuperview()
|
|
}
|
|
remarkTitleLabel.snp.makeConstraints { make in
|
|
make.top.equalTo(amountGroupView.snp.bottom).offset(Layout.formGap)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
remarkContainerView.snp.makeConstraints { make in
|
|
make.top.equalTo(remarkTitleLabel.snp.bottom).offset(AppSpacing.xs)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
make.height.equalTo(Layout.remarkHeight)
|
|
}
|
|
remarkTextView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
}
|
|
remarkPlaceholderLabel.snp.makeConstraints { make in
|
|
make.top.equalToSuperview().offset(16)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.sm)
|
|
}
|
|
actionRow.snp.makeConstraints { make in
|
|
make.top.equalTo(remarkContainerView.snp.bottom).offset(Layout.actionTop)
|
|
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
make.bottom.equalToSuperview().inset(Layout.bottomInset)
|
|
}
|
|
[cancelButton, confirmButton].forEach { button in
|
|
button.snp.makeConstraints { make in
|
|
make.width.equalTo(84)
|
|
make.height.equalTo(38)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func configureSectionLabel(_ label: UILabel, text: String) {
|
|
label.text = text
|
|
label.font = .systemFont(ofSize: 16)
|
|
label.textColor = UIColor(hex: 0x4B5563)
|
|
}
|
|
|
|
private func configureModeButton(_ button: UIButton, title: String) {
|
|
button.setTitle(title, for: .normal)
|
|
button.titleLabel?.font = .systemFont(ofSize: 14)
|
|
button.layer.cornerRadius = AppRadius.sm
|
|
button.layer.borderWidth = 1
|
|
}
|
|
|
|
private func configureActionButton(_ button: UIButton, title: String, foreground: UIColor, background: UIColor) {
|
|
button.setTitle(title, for: .normal)
|
|
button.setTitleColor(foreground, for: .normal)
|
|
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
|
button.backgroundColor = background
|
|
button.layer.cornerRadius = AppRadius.sm
|
|
}
|
|
|
|
private func applyMode(animated: Bool) {
|
|
let amountHidden = selectedMode == .full
|
|
amountGroupView.isHidden = amountHidden
|
|
amountGroupView.snp.updateConstraints { make in
|
|
make.height.equalTo(amountHidden ? 0 : Layout.amountGroupHeight)
|
|
}
|
|
remarkTitleLabel.snp.remakeConstraints { make in
|
|
make.top.equalTo((amountHidden ? modeButtonStack : amountGroupView).snp.bottom).offset(Layout.formGap)
|
|
make.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
|
}
|
|
|
|
updateModeButton(fullRefundButton, selected: selectedMode == .full)
|
|
updateModeButton(partialRefundButton, selected: selectedMode == .partial)
|
|
let changes = { self.layoutIfNeeded() }
|
|
if animated {
|
|
UIView.animate(withDuration: 0.2, animations: changes)
|
|
} else {
|
|
changes()
|
|
}
|
|
}
|
|
|
|
private func updateModeButton(_ button: UIButton, selected: Bool) {
|
|
button.backgroundColor = selected ? AppColor.primary : .white
|
|
button.layer.borderColor = selected ? UIColor.clear.cgColor : UIColor(hex: 0xB6BECA).cgColor
|
|
button.setTitleColor(selected ? .white : UIColor(hex: 0x4B5563), for: .normal)
|
|
}
|
|
|
|
func textViewDidChange(_ textView: UITextView) {
|
|
if textView.text.count > 255 {
|
|
textView.text = String(textView.text.prefix(255))
|
|
}
|
|
remarkPlaceholderLabel.isHidden = !textView.text.isEmpty
|
|
}
|
|
|
|
@objc private func amountChanged() {
|
|
let filtered = OrderRefundDialogValidation.filterMoneyInput(
|
|
amountField.text ?? "",
|
|
maxLength: configuration.moneyMaxLength
|
|
)
|
|
if amountField.text != filtered {
|
|
amountField.text = filtered
|
|
}
|
|
}
|
|
|
|
@objc private func fullRefundTapped() {
|
|
selectedMode = .full
|
|
amountField.text = configuration.kind == .storeOrder ? configuration.maxRefundAmount : configuration.fullRefundAmount
|
|
applyMode(animated: true)
|
|
}
|
|
|
|
@objc private func partialRefundTapped() {
|
|
selectedMode = .partial
|
|
amountField.text = ""
|
|
applyMode(animated: true)
|
|
amountField.becomeFirstResponder()
|
|
}
|
|
|
|
@objc private func cancelTapped() {
|
|
dismiss()
|
|
}
|
|
|
|
@objc private func confirmTapped() {
|
|
let result = OrderRefundDialogValidation.validate(
|
|
kind: configuration.kind,
|
|
mode: selectedMode,
|
|
amountText: amountField.text ?? "",
|
|
reason: remarkTextView.text ?? "",
|
|
fullRefundAmount: configuration.fullRefundAmount,
|
|
maxRefundAmount: configuration.maxRefundAmount
|
|
)
|
|
switch result {
|
|
case .success(let payload):
|
|
onConfirm?(payload)
|
|
dismiss()
|
|
case .failure(let error):
|
|
onMessage?(error.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum OrderSignInAlertController {
|
|
static func present(from presenter: UIViewController, response: OrderSignInResponse) {
|
|
let message = """
|
|
项目:\(response.projectName)
|
|
景区:\(response.scenicName)
|
|
金额:¥\(response.orderAmount)
|
|
手机号:\(response.userPhone)
|
|
"""
|
|
let alert = UIAlertController(title: response.checkIn ? "签到成功" : "核销结果", message: message, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: "确定", style: .default))
|
|
presenter.present(alert, animated: true)
|
|
}
|
|
}
|
|
|
|
enum WriteOffResultAlertController {
|
|
static func present(from presenter: UIViewController, response: WriteOffOrderResponse) {
|
|
let message = """
|
|
项目:\(response.projectName)
|
|
景区:\(response.scenicName)
|
|
金额:¥\(response.orderAmount)
|
|
手机号:\(response.userPhone)
|
|
"""
|
|
let alert = UIAlertController(title: response.orderVerify ? "核销成功" : "核销结果", message: message, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: "确定", style: .default))
|
|
presenter.present(alert, animated: true)
|
|
}
|
|
}
|
|
|
|
enum SendGiftAlertController {
|
|
static func present(
|
|
from presenter: UIViewController,
|
|
photoNum: Int,
|
|
videoNum: Int,
|
|
onConfirm: @escaping (Int, Int) -> Void
|
|
) {
|
|
let alert = UIAlertController(title: "赠送修图", message: nil, preferredStyle: .alert)
|
|
alert.addTextField { $0.placeholder = "赠送照片数"; $0.keyboardType = .numberPad; $0.text = "\(photoNum)" }
|
|
alert.addTextField { $0.placeholder = "赠送视频数"; $0.keyboardType = .numberPad; $0.text = "\(videoNum)" }
|
|
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
alert.addAction(UIAlertAction(title: "确认", style: .default) { _ in
|
|
let photo = Int(alert.textFields?.first?.text ?? "0") ?? 0
|
|
let video = Int(alert.textFields?.last?.text ?? "0") ?? 0
|
|
onConfirm(photo, video)
|
|
})
|
|
presenter.present(alert, animated: true)
|
|
}
|
|
}
|