// // 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 let mode: TravelAlbumEntryViewModel.CreateMode = .preShoot private let scrollView = UIScrollView() private let contentView = UIView() private let titleLabel = UILabel() private let preShootCard = TravelAlbumModeOptionView() private let fieldsStack = UIStackView() private let freeCountField = UITextField() private let singlePriceField = UITextField() private let packagePriceField = UITextField() 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 ) fieldsStack.axis = .vertical fieldsStack.spacing = 14 configureTextField(freeCountField, placeholder: "请输入免费张数", keyboardType: .numberPad) configureTextField(singlePriceField, placeholder: "请输入单张照片价格", keyboardType: .decimalPad) configureTextField(packagePriceField, placeholder: "请输入打包价格", keyboardType: .decimalPad) 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, 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) } fieldsStack.snp.makeConstraints { make in make.top.equalTo(preShootCard.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() { 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() } fieldsStack.addArrangedSubview(makeFieldGroup(title: "免费张数", required: false, field: freeCountField)) fieldsStack.addArrangedSubview(makeFieldGroup(title: "单张照片价格(元)", required: true, field: singlePriceField)) fieldsStack.addArrangedSubview(makeFieldGroup(title: "打包价格(元)", required: false, field: packagePriceField)) } 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 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 } @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: nil, 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[..