Files
suixinkan_uikit/suixinkan/UI/TravelAlbum/CreateTravelAlbumSheetViewController.swift
2026-07-07 17:23:31 +08:00

394 lines
16 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CreateTravelAlbumSheetViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Sheet Android `CreateTravelAlbumModal`
final class CreateTravelAlbumSheetViewController: BaseViewController {
var onDismissed: (() -> Void)?
private let viewModel: TravelAlbumEntryViewModel
private let api: any TravelAlbumServing
private var mode: TravelAlbumEntryViewModel.CreateMode = .preShoot
private var selectedOrder: TravelAlbumAvailableOrder?
private let scrollView = UIScrollView()
private let contentView = UIView()
private let titleLabel = UILabel()
private let preShootCard = TravelAlbumModeOptionView()
private let preOrderCard = TravelAlbumModeOptionView()
private let fieldsStack = UIStackView()
private let freeCountField = UITextField()
private let singlePriceField = UITextField()
private let packagePriceField = UITextField()
private let orderButton = UIButton(type: .system)
private let cancelButton = UIButton(type: .system)
private let confirmButton = UIButton(type: .system)
init(viewModel: TravelAlbumEntryViewModel, api: any TravelAlbumServing) {
self.viewModel = viewModel
self.api = api
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .pageSheet
if let sheetPresentationController {
sheetPresentationController.detents = [.medium(), .large()]
sheetPresentationController.prefersGrabberVisible = false
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {}
override func setupUI() {
view.backgroundColor = .white
titleLabel.text = "新建相册任务"
titleLabel.font = .systemFont(ofSize: 18, weight: .semibold)
titleLabel.textColor = AppColor.textPrimary
titleLabel.textAlignment = .center
preShootCard.apply(
title: "先拍再买",
desc: "拍完后分享给用户,用户在小程序上选择性购买",
selected: true
)
preOrderCard.apply(
title: "买了再拍",
desc: "表示用户已经在小程序上下过单了,绑定对应订单上传后用户可以直接选片",
selected: false
)
fieldsStack.axis = .vertical
fieldsStack.spacing = 14
configureTextField(freeCountField, placeholder: "请输入免费张数", keyboardType: .numberPad)
configureTextField(singlePriceField, placeholder: "请输入单张照片价格", keyboardType: .decimalPad)
configureTextField(packagePriceField, placeholder: "请输入打包价格", keyboardType: .decimalPad)
orderButton.contentHorizontalAlignment = .left
orderButton.setTitle("请选择订单", for: .normal)
orderButton.setTitleColor(AppColor.textTertiary, for: .normal)
orderButton.titleLabel?.font = .systemFont(ofSize: 14)
orderButton.layer.cornerRadius = 8
orderButton.layer.borderColor = AppColor.border.cgColor
orderButton.layer.borderWidth = 1
orderButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
configureActionButton(cancelButton, title: "取消", backgroundColor: UIColor(hex: 0xF4F4F4), titleColor: AppColor.textSecondary)
configureActionButton(confirmButton, title: "确定", backgroundColor: AppColor.primary, titleColor: .white)
view.addSubview(scrollView)
scrollView.addSubview(contentView)
[titleLabel, preShootCard, preOrderCard, fieldsStack, cancelButton, confirmButton].forEach(contentView.addSubview)
rebuildFields()
}
override func setupConstraints() {
scrollView.snp.makeConstraints { make in
make.edges.equalTo(view.safeAreaLayoutGuide)
}
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(18)
make.leading.trailing.equalToSuperview().inset(20)
}
preShootCard.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(18)
make.leading.trailing.equalToSuperview().inset(16)
}
preOrderCard.snp.makeConstraints { make in
make.top.equalTo(preShootCard.snp.bottom).offset(10)
make.leading.trailing.equalTo(preShootCard)
}
fieldsStack.snp.makeConstraints { make in
make.top.equalTo(preOrderCard.snp.bottom).offset(18)
make.leading.trailing.equalTo(preShootCard)
}
cancelButton.snp.makeConstraints { make in
make.top.equalTo(fieldsStack.snp.bottom).offset(22)
make.leading.equalToSuperview().offset(16)
make.height.equalTo(48)
make.width.equalTo(confirmButton)
make.bottom.equalToSuperview().offset(-16)
}
confirmButton.snp.makeConstraints { make in
make.top.height.width.equalTo(cancelButton)
make.leading.equalTo(cancelButton.snp.trailing).offset(12)
make.trailing.equalToSuperview().offset(-16)
}
}
override func bindActions() {
preShootCard.addTarget(self, action: #selector(preShootTapped), for: .touchUpInside)
preOrderCard.addTarget(self, action: #selector(preOrderTapped), for: .touchUpInside)
orderButton.addTarget(self, action: #selector(orderTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
[freeCountField, singlePriceField, packagePriceField].forEach {
$0.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isBeingDismissed {
onDismissed?()
}
}
private func configureTextField(_ field: UITextField, placeholder: String, keyboardType: UIKeyboardType) {
field.placeholder = placeholder
field.keyboardType = keyboardType
field.font = .systemFont(ofSize: 14)
field.layer.cornerRadius = 8
field.layer.borderWidth = 1
field.layer.borderColor = AppColor.border.cgColor
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 1))
field.leftViewMode = .always
field.snp.makeConstraints { make in
make.height.equalTo(44)
}
}
private func configureActionButton(_ button: UIButton, title: String, backgroundColor: UIColor, titleColor: UIColor) {
button.setTitle(title, for: .normal)
button.setTitleColor(titleColor, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
button.backgroundColor = backgroundColor
button.layer.cornerRadius = 10
}
private func rebuildFields() {
fieldsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
if mode == .preShoot {
fieldsStack.addArrangedSubview(makeFieldGroup(title: "免费张数", required: false, field: freeCountField))
fieldsStack.addArrangedSubview(makeFieldGroup(title: "单张照片价格(元)", required: true, field: singlePriceField))
fieldsStack.addArrangedSubview(makeFieldGroup(title: "打包价格(元)", required: false, field: packagePriceField))
} else {
fieldsStack.addArrangedSubview(makeOrderGroup())
}
}
private func makeFieldGroup(title: String, required: Bool, field: UITextField) -> UIView {
let container = UIView()
let label = UILabel()
label.attributedText = fieldTitle(title, required: required)
container.addSubview(label)
container.addSubview(field)
label.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
field.snp.makeConstraints { make in
make.top.equalTo(label.snp.bottom).offset(8)
make.leading.trailing.bottom.equalToSuperview()
}
return container
}
private func makeOrderGroup() -> UIView {
let container = UIView()
let label = UILabel()
label.attributedText = fieldTitle("绑定订单", required: false)
container.addSubview(label)
container.addSubview(orderButton)
label.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
orderButton.snp.makeConstraints { make in
make.top.equalTo(label.snp.bottom).offset(8)
make.leading.trailing.bottom.equalToSuperview()
make.height.greaterThanOrEqualTo(46)
}
return container
}
private func fieldTitle(_ text: String, required: Bool) -> NSAttributedString {
let result = NSMutableAttributedString(
string: text,
attributes: [.font: UIFont.systemFont(ofSize: 14, weight: .medium), .foregroundColor: AppColor.textPrimary]
)
if required {
result.append(NSAttributedString(string: " *", attributes: [.foregroundColor: UIColor.red]))
}
return result
}
private func updateMode(_ newMode: TravelAlbumEntryViewModel.CreateMode) {
mode = newMode
preShootCard.apply(
title: "先拍再买",
desc: "拍完后分享给用户,用户在小程序上选择性购买",
selected: mode == .preShoot
)
preOrderCard.apply(
title: "买了再拍",
desc: "表示用户已经在小程序上下过单了,绑定对应订单上传后用户可以直接选片",
selected: mode == .preOrder
)
rebuildFields()
}
private func updateOrderButton() {
guard let order = selectedOrder else {
orderButton.setTitle("请选择订单", for: .normal)
orderButton.setTitleColor(AppColor.textTertiary, for: .normal)
return
}
let title = "\(order.projectName.isEmpty ? "未命名项目" : order.projectName)\n手机号:\(order.userPhone)\n订单号:\(order.orderNumber)"
orderButton.setTitle(title, for: .normal)
orderButton.setTitleColor(AppColor.textPrimary, for: .normal)
orderButton.titleLabel?.numberOfLines = 3
}
@objc private func preShootTapped() {
updateMode(.preShoot)
}
@objc private func preOrderTapped() {
updateMode(.preOrder)
}
@objc private func orderTapped() {
let alert = UIAlertController(title: "绑定订单", message: nil, preferredStyle: .actionSheet)
if viewModel.availableOrders.isEmpty {
alert.addAction(UIAlertAction(title: "暂无可绑定订单", style: .default))
} else {
viewModel.availableOrders.forEach { order in
let title = "\(order.projectName.isEmpty ? "未命名项目" : order.projectName) \(order.userPhone)"
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.selectedOrder = order
self?.updateOrderButton()
})
}
}
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
@objc private func cancelTapped() {
dismiss(animated: true)
}
@objc private func confirmTapped() {
Task {
await viewModel.confirmCreate(
mode: mode,
freeCount: freeCountField.text ?? "",
singlePrice: singlePriceField.text ?? "",
packagePrice: packagePriceField.text ?? "",
order: selectedOrder,
api: api
)
await MainActor.run {
if !viewModel.isCreateSheetVisible {
dismiss(animated: true)
}
}
}
}
@objc private func textFieldEditingChanged(_ field: UITextField) {
let text = field.text ?? ""
if field === freeCountField {
field.text = text.filter(\.isNumber)
} else {
field.text = sanitizeMoneyInput(text)
}
}
private func sanitizeMoneyInput(_ value: String) -> String {
var result = ""
var hasDot = false
for char in value {
if char.isNumber {
result.append(char)
} else if char == ".", !hasDot {
result.append(char)
hasDot = true
}
}
guard !result.isEmpty else { return "" }
if result.hasPrefix(".") {
result = "0" + result
}
guard let dotIndex = result.firstIndex(of: ".") else {
let trimmed = result.drop { $0 == "0" }
return trimmed.isEmpty ? "0" : String(trimmed)
}
let integerPart = result[..<dotIndex].drop { $0 == "0" }
let decimalStart = result.index(after: dotIndex)
let decimal = result[decimalStart...].prefix(2)
return "\(integerPart.isEmpty ? "0" : String(integerPart)).\(decimal)"
}
}
///
private final class TravelAlbumModeOptionView: UIControl {
private let dotView = UIView()
private let innerDotView = UIView()
private let titleLabel = UILabel()
private let descLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 10
layer.borderWidth = 1
dotView.layer.cornerRadius = 10
innerDotView.backgroundColor = .white
innerDotView.layer.cornerRadius = 3.5
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
descLabel.font = .systemFont(ofSize: 12)
descLabel.textColor = AppColor.textSecondary
descLabel.numberOfLines = 0
addSubview(dotView)
dotView.addSubview(innerDotView)
addSubview(titleLabel)
addSubview(descLabel)
dotView.snp.makeConstraints { make in
make.leading.top.equalToSuperview().offset(14)
make.size.equalTo(20)
}
innerDotView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(7)
}
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(14)
make.leading.equalTo(dotView.snp.trailing).offset(10)
make.trailing.equalToSuperview().offset(-14)
}
descLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.leading.trailing.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-14)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(title: String, desc: String, selected: Bool) {
titleLabel.text = title
descLabel.text = desc
backgroundColor = selected ? AppColor.primary.withAlphaComponent(0.06) : .white
layer.borderColor = (selected ? AppColor.primary : AppColor.border).cgColor
titleLabel.textColor = selected ? AppColor.primary : AppColor.textPrimary
dotView.backgroundColor = selected ? AppColor.primary : .clear
dotView.layer.borderWidth = selected ? 0 : 2
dotView.layer.borderColor = AppColor.border.cgColor
innerDotView.isHidden = !selected
}
}