Add scenic selection and cooperation order flows aligned with Android.

Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 10:48:43 +08:00
parent 3c4ca7eefb
commit 2bea05b1d9
50 changed files with 5603 additions and 191 deletions

View File

@ -0,0 +1,252 @@
//
// PermissionApplyViews.swift
// suixinkan
//
import SnapKit
import UIKit
/// toast
final class PermissionApplyBannerView: UIView {
var onApplyNewScenic: (() -> Void)?
private let titleLabel = UILabel()
private let applyButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.warningBackground
titleLabel.text = "希望开通全新景区?"
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textColor = AppColor.warning
applyButton.setTitle("新景区申请 ", for: .normal)
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
applyButton.setTitleColor(AppColor.warning, for: .normal)
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
addSubview(titleLabel)
addSubview(applyButton)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
}
applyButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
}
snp.makeConstraints { make in
make.height.equalTo(44)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func applyTapped() {
onApplyNewScenic?()
}
}
///
final class PermissionFormSelectorRow: UIView {
var onTap: (() -> Void)?
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let valueLabel = UILabel()
private let chevron = UIImageView(image: UIImage(systemName: "chevron.down"))
init(title: String, subtitle: String, placeholder: String) {
super.init(frame: .zero)
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
titleLabel.textColor = AppColor.textPrimary
subtitleLabel.text = subtitle
subtitleLabel.font = .systemFont(ofSize: 12)
subtitleLabel.textColor = AppColor.textTertiary
valueLabel.text = placeholder
valueLabel.font = .systemFont(ofSize: 14)
valueLabel.textColor = AppColor.textTertiary
chevron.tintColor = AppColor.textTertiary
chevron.contentMode = .scaleAspectFit
let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped))
addGestureRecognizer(tap)
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(valueLabel)
addSubview(chevron)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview()
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(4)
make.leading.equalToSuperview()
}
valueLabel.snp.makeConstraints { make in
make.top.equalTo(subtitleLabel.snp.bottom).offset(12)
make.leading.bottom.equalToSuperview()
make.trailing.lessThanOrEqualTo(chevron.snp.leading).offset(-8)
}
chevron.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalTo(valueLabel)
make.size.equalTo(14)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setValue(_ text: String, isPlaceholder: Bool) {
valueLabel.text = text
valueLabel.textColor = isPlaceholder ? AppColor.textTertiary : AppColor.textPrimary
}
@objc private func rowTapped() {
onTap?()
}
}
///
final class PermissionSelectedTagsView: UIView {
private let titleLabel = UILabel()
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textColor = AppColor.textPrimary
stackView.axis = .vertical
stackView.spacing = 8
addSubview(titleLabel)
addSubview(stackView)
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
stackView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.leading.trailing.bottom.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(title: String, names: [String], onRemove: ((Int) -> Void)? = nil) {
titleLabel.text = title
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
guard !names.isEmpty else {
isHidden = true
return
}
isHidden = false
let row = UIStackView()
row.axis = .horizontal
row.spacing = 8
row.alignment = .leading
names.enumerated().forEach { index, name in
let chip = PermissionTagChip(title: name)
if let onRemove {
chip.onRemove = { onRemove(index) }
}
row.addArrangedSubview(chip)
}
stackView.addArrangedSubview(row)
}
}
/// Chip
final class PermissionTagChip: UIView {
var onRemove: (() -> Void)?
private let label = UILabel()
private let removeButton = UIButton(type: .system)
init(title: String, showsRemove: Bool = true) {
super.init(frame: .zero)
layer.cornerRadius = 4
layer.borderWidth = 1
layer.borderColor = AppColor.primary.cgColor
backgroundColor = .white
label.text = title
label.font = .systemFont(ofSize: 14)
label.textColor = AppColor.primary
removeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
removeButton.tintColor = AppColor.primary
removeButton.addTarget(self, action: #selector(removeTapped), for: .touchUpInside)
removeButton.isHidden = !showsRemove
addSubview(label)
addSubview(removeButton)
label.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.top.bottom.equalToSuperview().inset(8)
if showsRemove {
make.trailing.equalTo(removeButton.snp.leading).offset(-4)
} else {
make.trailing.equalToSuperview().offset(-12)
}
}
removeButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-8)
make.centerY.equalToSuperview()
make.size.equalTo(20)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func removeTapped() {
onRemove?()
}
}
///
final class PermissionSubmitButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setTitle("提交申请", for: .normal)
setTitleColor(.white, for: .normal)
titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
backgroundColor = AppColor.primary
layer.cornerRadius = 8
snp.makeConstraints { make in
make.height.equalTo(48)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSubmitting(_ submitting: Bool) {
isEnabled = !submitting
alpha = submitting ? 0.6 : 1
setTitle(submitting ? "提交中..." : "提交申请", for: .normal)
}
}

View File

@ -0,0 +1,300 @@
//
// ScenicSelectionViews.swift
// suixinkan
//
import Kingfisher
import SnapKit
import UIKit
///
final class ScenicPermissionBannerView: UIView {
var onApplyNow: (() -> Void)?
private let titleLabel = UILabel()
private let applyButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = AppColor.warningBackground
titleLabel.text = "希望开通更多景区权限"
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
titleLabel.textColor = AppColor.warning
titleLabel.numberOfLines = 2
applyButton.setTitle("立即申请 ", for: .normal)
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
applyButton.setTitleColor(AppColor.warning, for: .normal)
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
addSubview(titleLabel)
addSubview(applyButton)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualTo(applyButton.snp.leading).offset(-8)
}
applyButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
}
snp.makeConstraints { make in
make.height.greaterThanOrEqualTo(44)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func applyTapped() {
onApplyNow?()
}
}
///
final class ScenicSelectionSearchBar: UIView, UITextFieldDelegate {
var onSearchTextChange: ((String) -> Void)?
let textField = UITextField()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hex: 0xF2F4F8)
layer.cornerRadius = 12
clipsToBounds = true
let icon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
icon.tintColor = UIColor(hex: 0xB3B8C2)
icon.contentMode = .scaleAspectFit
textField.placeholder = "搜索景区"
textField.font = .systemFont(ofSize: 14)
textField.textColor = AppColor.textPrimary
textField.returnKeyType = .search
textField.delegate = self
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
addSubview(icon)
addSubview(textField)
icon.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(12)
make.centerY.equalToSuperview()
make.size.equalTo(18)
}
textField.snp.makeConstraints { make in
make.leading.equalTo(icon.snp.trailing).offset(8)
make.trailing.equalToSuperview().offset(-12)
make.top.bottom.equalToSuperview().inset(10)
}
snp.makeConstraints { make in
make.height.equalTo(44)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func textChanged() {
onSearchTextChange?(textField.text ?? "")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
///
final class ScenicCurrentLocationBar: UIView {
var onRelocate: (() -> Void)?
private let iconView = UIImageView(image: UIImage(systemName: "location.fill"))
private let locationLabel = UILabel()
private let relocateButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
locationLabel.font = .systemFont(ofSize: 14)
locationLabel.textColor = AppColor.textPrimary
locationLabel.numberOfLines = 2
relocateButton.setTitle("重新定位", for: .normal)
relocateButton.titleLabel?.font = .systemFont(ofSize: 12)
relocateButton.setTitleColor(AppColor.primary, for: .normal)
relocateButton.backgroundColor = AppColor.primaryLight
relocateButton.layer.cornerRadius = 4
relocateButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8)
relocateButton.addTarget(self, action: #selector(relocateTapped), for: .touchUpInside)
relocateButton.setContentHuggingPriority(.required, for: .horizontal)
relocateButton.setContentCompressionResistancePriority(.required, for: .horizontal)
addSubview(iconView)
addSubview(locationLabel)
addSubview(relocateButton)
iconView.snp.makeConstraints { make in
make.leading.equalToSuperview()
make.centerY.equalToSuperview()
make.size.equalTo(16)
}
locationLabel.snp.makeConstraints { make in
make.leading.equalTo(iconView.snp.trailing).offset(4)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualTo(relocateButton.snp.leading).offset(-12)
}
relocateButton.snp.makeConstraints { make in
make.trailing.equalToSuperview()
make.centerY.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(locationText: String) {
locationLabel.text = locationText
}
@objc private func relocateTapped() {
onRelocate?()
}
}
/// Cell
final class ScenicSpotCell: UITableViewCell {
static let reuseIdentifier = "ScenicSpotCell"
var onTap: (() -> Void)?
private let cardView = UIView()
private let coverImageView = UIImageView()
private let nameLabel = UILabel()
private let distanceLabel = UILabel()
private let addressLabel = UILabel()
private let statusTag = UILabel()
private let selectedBorderColor = UIColor(hex: 0xE74C3C)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.backgroundColor = .clear
cardView.backgroundColor = AppColor.pageBackgroundSoft
cardView.layer.cornerRadius = 8
cardView.layer.borderWidth = 2
cardView.layer.borderColor = UIColor(hex: 0xF4F4F4).cgColor
coverImageView.contentMode = .scaleAspectFill
coverImageView.clipsToBounds = true
coverImageView.layer.cornerRadius = 6
coverImageView.backgroundColor = UIColor(hex: 0xF0F0F0)
nameLabel.font = .systemFont(ofSize: 16, weight: .bold)
nameLabel.textColor = AppColor.textPrimary
distanceLabel.font = .systemFont(ofSize: 12)
distanceLabel.textColor = AppColor.textSecondary
addressLabel.font = .systemFont(ofSize: 12)
addressLabel.textColor = AppColor.textTertiary
addressLabel.numberOfLines = 2
statusTag.font = .systemFont(ofSize: 11, weight: .medium)
statusTag.textColor = .white
statusTag.textAlignment = .center
statusTag.layer.cornerRadius = 4
statusTag.clipsToBounds = true
statusTag.isHidden = true
contentView.addSubview(cardView)
cardView.addSubview(coverImageView)
cardView.addSubview(nameLabel)
cardView.addSubview(statusTag)
cardView.addSubview(distanceLabel)
cardView.addSubview(addressLabel)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
}
coverImageView.snp.makeConstraints { make in
make.leading.top.bottom.equalToSuperview().inset(12)
make.size.equalTo(80)
}
nameLabel.snp.makeConstraints { make in
make.leading.equalTo(coverImageView.snp.trailing).offset(12)
make.top.equalToSuperview().offset(12)
make.trailing.lessThanOrEqualTo(statusTag.snp.leading).offset(-8)
}
statusTag.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-12)
make.centerY.equalTo(nameLabel)
make.height.equalTo(20)
}
distanceLabel.snp.makeConstraints { make in
make.leading.equalTo(nameLabel)
make.top.equalTo(nameLabel.snp.bottom).offset(6)
}
addressLabel.snp.makeConstraints { make in
make.leading.equalTo(nameLabel)
make.trailing.equalToSuperview().offset(-12)
make.top.equalTo(distanceLabel.snp.bottom).offset(4)
make.bottom.lessThanOrEqualToSuperview().offset(-12)
}
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
cardView.addGestureRecognizer(tap)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(_ item: ScenicSpotDisplayItem) {
nameLabel.text = item.name
distanceLabel.text = "\(item.distanceText)km"
addressLabel.text = item.address
if let url = URL(string: item.coverImg), !item.coverImg.isEmpty {
coverImageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo"))
} else {
coverImageView.image = UIImage(systemName: "photo")
coverImageView.tintColor = AppColor.textTertiary
}
if let label = item.statusLabel {
statusTag.isHidden = false
statusTag.text = " \(label) "
switch item.openStatus {
case .nearest, .closed:
statusTag.backgroundColor = selectedBorderColor
case .none:
statusTag.backgroundColor = AppColor.textTertiary
}
} else {
statusTag.isHidden = true
}
cardView.layer.borderColor = (item.isSelected ? selectedBorderColor : UIColor(hex: 0xF4F4F4)).cgColor
}
@objc private func cardTapped() {
onTap?()
}
}