// // ScenicApplicationViews.swift // suixinkan // import Kingfisher import SnapKit import UIKit /// 温馨提示卡片,对齐 Android `WarmReminderCard`。 final class ScenicApplicationWarmReminderView: UIView { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = AppColor.primaryBanner layer.cornerRadius = 8 let titleLabel = UILabel() titleLabel.text = "温馨提示" titleLabel.font = .systemFont(ofSize: 16, weight: .bold) titleLabel.textColor = .white let bodyLabel = UILabel() bodyLabel.text = "如果你想入驻更多的景区,平台可以帮你跟景区官方谈合作,如果已经有的景区,只要你符合入驻的条件,即可入驻。" bodyLabel.font = .systemFont(ofSize: 14) bodyLabel.textColor = .white bodyLabel.numberOfLines = 0 let stack = UIStackView(arrangedSubviews: [titleLabel, bodyLabel]) stack.axis = .vertical stack.spacing = 8 addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } /// 审核状态卡片,对齐 Android `ScenicApplicationAuditStatusCard`。 final class ScenicApplicationAuditStatusView: UIView { func apply(pending: ScenicApplicationPendingResponse) { subviews.forEach { $0.removeFromSuperview() } let statusLabel: String let textColor: UIColor let background: UIColor switch pending.status { case 1: statusLabel = "待审核" textColor = AppColor.warning background = AppColor.warningBackground case 2: statusLabel = "通过" textColor = AppColor.info background = AppColor.primaryLight case 3, 9: statusLabel = pending.status == 3 ? "拒绝" : "取消" textColor = AppColor.danger background = AppColor.dangerBackground default: statusLabel = "--" textColor = AppColor.warning background = AppColor.warningBackground } backgroundColor = background let stack = UIStackView() stack.axis = .vertical stack.spacing = 8 stack.addArrangedSubview(makeRow(label: "审核状态", value: statusLabel, color: textColor)) if pending.status == 2 { stack.addArrangedSubview(makeRow(label: "审核人", value: pending.auditedBy ?? "--", color: textColor)) stack.addArrangedSubview(makeRow(label: "审核时间", value: pending.auditedAt ?? "--", color: textColor)) } let note = pending.rejectReason ?? pending.auditNote ?? "--" stack.addArrangedSubview(makeRow(label: "备注", value: note.isEmpty ? "--" : note, color: textColor)) addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 16, left: 20, bottom: 16, right: 20)) } } private func makeRow(label: String, value: String, color: UIColor) -> UIView { let row = UIStackView() row.axis = .horizontal row.distribution = .equalSpacing let left = UILabel() left.text = label left.font = .systemFont(ofSize: 14) left.textColor = color let right = UILabel() right.text = value right.font = .systemFont(ofSize: 14) right.textColor = color right.textAlignment = .right right.numberOfLines = 0 row.addArrangedSubview(left) row.addArrangedSubview(right) return row } } /// 表单字段标题 + 内容容器。 final class ScenicApplicationFormFieldView: UIView { private let titleLabel = UILabel() private let contentContainer = UIView() init(title: String) { super.init(frame: .zero) titleLabel.text = title titleLabel.font = .systemFont(ofSize: 16, weight: .medium) titleLabel.textColor = AppColor.textPrimary let stack = UIStackView(arrangedSubviews: [titleLabel, contentContainer]) stack.axis = .vertical stack.spacing = 8 addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setContent(_ view: UIView) { contentContainer.subviews.forEach { $0.removeFromSuperview() } contentContainer.addSubview(view) view.snp.makeConstraints { make in make.edges.equalToSuperview() } } func updateTitle(_ title: String) { titleLabel.text = title } } /// 省/市选择按钮。 final class ScenicApplicationLocationSelector: UIControl { private let valueLabel = UILabel() private let chevron = UIImageView(image: UIImage(systemName: "chevron.down")) override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white layer.cornerRadius = 8 layer.borderWidth = 1 layer.borderColor = AppColor.border.cgColor valueLabel.font = .systemFont(ofSize: 14) valueLabel.textColor = AppColor.textTertiary valueLabel.text = "请选择" chevron.tintColor = AppColor.textTertiary chevron.contentMode = .scaleAspectFit addSubview(valueLabel) addSubview(chevron) valueLabel.snp.makeConstraints { make in make.leading.equalToSuperview().offset(12) make.centerY.equalToSuperview() make.trailing.lessThanOrEqualTo(chevron.snp.leading).offset(-8) } chevron.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-12) make.centerY.equalToSuperview() make.size.equalTo(16) } snp.makeConstraints { make in make.height.equalTo(44) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setValue(_ value: String?, placeholder: String) { if let value, !value.isEmpty { valueLabel.text = value valueLabel.textColor = AppColor.textPrimary } else { valueLabel.text = placeholder valueLabel.textColor = AppColor.textTertiary } } override var isEnabled: Bool { didSet { alpha = isEnabled ? 1 : 0.5 } } } /// 合作类型单选项。 final class ScenicApplicationCooperationOptionView: UIControl { private let titleLabel = UILabel() override var isSelected: Bool { didSet { applyStyle() } } init(title: String) { super.init(frame: .zero) titleLabel.text = title titleLabel.font = .systemFont(ofSize: 14) titleLabel.numberOfLines = 0 layer.cornerRadius = 8 layer.borderWidth = 2 addSubview(titleLabel) titleLabel.snp.makeConstraints { make in make.edges.equalToSuperview().inset(16) } applyStyle() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func applyStyle() { if isSelected { backgroundColor = AppColor.primaryBanner titleLabel.textColor = .white layer.borderColor = UIColor.clear.cgColor } else { backgroundColor = .white titleLabel.textColor = AppColor.textSecondary layer.borderColor = AppColor.inputBackground.cgColor } } } /// 横向图片上传区域。 final class ScenicApplicationImageUploadView: UIView { var onAdd: (() -> Void)? var onDelete: ((Int) -> Void)? var onRetry: ((Int) -> Void)? private let scrollView = UIScrollView() private let stackView = UIStackView() override init(frame: CGRect) { super.init(frame: frame) stackView.axis = .horizontal stackView.spacing = 8 stackView.alignment = .center scrollView.showsHorizontalScrollIndicator = false scrollView.addSubview(stackView) addSubview(scrollView) scrollView.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalTo(80) } stackView.snp.makeConstraints { make in make.edges.equalToSuperview() make.height.equalToSuperview() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(images: [ScenicApplicationImageItem], isReadOnly: Bool) { stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } for (index, item) in images.enumerated() { let cell = ScenicApplicationImageCell() cell.apply(item: item, showDelete: !isReadOnly) cell.onDelete = { [weak self] in self?.onDelete?(index) } cell.onRetry = { [weak self] in self?.onRetry?(index) } stackView.addArrangedSubview(cell) cell.snp.makeConstraints { make in make.size.equalTo(80) } } if images.count < 20, !isReadOnly { let addButton = ScenicApplicationAddImageButton() addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside) stackView.addArrangedSubview(addButton) addButton.snp.makeConstraints { make in make.size.equalTo(80) } } } @objc private func addTapped() { onAdd?() } } private final class ScenicApplicationAddImageButton: UIControl { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = AppColor.inputBackground layer.cornerRadius = 8 let icon = UIImageView(image: UIImage(systemName: "plus")) icon.tintColor = UIColor(hex: 0xB6BECA) let label = UILabel() label.text = "点击上传" label.font = .systemFont(ofSize: 12) label.textColor = UIColor(hex: 0xB6BECA) let stack = UIStackView(arrangedSubviews: [icon, label]) stack.axis = .vertical stack.spacing = 4 stack.alignment = .center stack.isUserInteractionEnabled = false addSubview(stack) stack.snp.makeConstraints { make in make.center.equalToSuperview() } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private final class ScenicApplicationImageCell: UIView { var onDelete: (() -> Void)? var onRetry: (() -> Void)? private let imageView = UIImageView() private let progressView = UIProgressView(progressViewStyle: .default) private let deleteButton = UIButton(type: .system) private let retryButton = UIButton(type: .system) private let errorLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) layer.cornerRadius = 8 clipsToBounds = true imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true deleteButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal) deleteButton.tintColor = .white deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside) retryButton.setTitle("重试", for: .normal) retryButton.titleLabel?.font = .systemFont(ofSize: 12) retryButton.setTitleColor(.white, for: .normal) retryButton.backgroundColor = UIColor.black.withAlphaComponent(0.5) retryButton.layer.cornerRadius = 4 retryButton.addTarget(self, action: #selector(retryTapped), for: .touchUpInside) retryButton.isHidden = true errorLabel.font = .systemFont(ofSize: 10) errorLabel.textColor = .white errorLabel.textAlignment = .center errorLabel.backgroundColor = UIColor.black.withAlphaComponent(0.5) errorLabel.isHidden = true addSubview(imageView) addSubview(progressView) addSubview(errorLabel) addSubview(retryButton) addSubview(deleteButton) imageView.snp.makeConstraints { make in make.edges.equalToSuperview() } progressView.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview().inset(4) } deleteButton.snp.makeConstraints { make in make.top.trailing.equalToSuperview().inset(2) make.size.equalTo(22) } retryButton.snp.makeConstraints { make in make.center.equalToSuperview() make.width.equalTo(44) make.height.equalTo(24) } errorLabel.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() make.height.equalTo(18) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(item: ScenicApplicationImageItem, showDelete: Bool) { deleteButton.isHidden = !showDelete progressView.isHidden = !item.isUploading progressView.progress = Float(item.uploadProgress) / 100 if let data = item.localData, let image = UIImage(data: data) { imageView.image = image } else if !item.remoteURL.isEmpty, let url = URL(string: item.remoteURL) { imageView.kf.setImage(with: url) } else { imageView.image = nil } if let error = item.errorMessage, !error.isEmpty { errorLabel.isHidden = false errorLabel.text = "失败" retryButton.isHidden = false } else { errorLabel.isHidden = true retryButton.isHidden = true } } @objc private func deleteTapped() { onDelete?() } @objc private func retryTapped() { onRetry?() } } /// 协议勾选行,对齐 Android `AgreementCheckbox`。 final class ScenicApplicationAgreementView: UIView { var onToggle: (() -> Void)? private let checkbox = UIButton(type: .custom) private var isChecked = false override init(frame: CGRect) { super.init(frame: frame) checkbox.setImage(UIImage(systemName: "square"), for: .normal) checkbox.setImage(UIImage(systemName: "checkmark.square.fill"), for: .selected) checkbox.tintColor = AppColor.primaryBanner checkbox.addTarget(self, action: #selector(toggleTapped), for: .touchUpInside) let prefix = UILabel() prefix.text = "已阅读并同意" prefix.font = .systemFont(ofSize: 14) prefix.textColor = AppColor.textSecondary let userNotice = UILabel() userNotice.text = "《用户须知》" userNotice.font = .systemFont(ofSize: 14) userNotice.textColor = AppColor.primaryBanner let middle = UILabel() middle.text = "与" middle.font = .systemFont(ofSize: 14) middle.textColor = AppColor.textSecondary let privacy = UILabel() privacy.text = "《隐私政策》" privacy.font = .systemFont(ofSize: 14) privacy.textColor = AppColor.primaryBanner let textStack = UIStackView(arrangedSubviews: [prefix, userNotice, middle, privacy]) textStack.axis = .horizontal textStack.spacing = 2 textStack.alignment = .center let row = UIStackView(arrangedSubviews: [checkbox, textStack]) row.axis = .horizontal row.spacing = 2 row.alignment = .center let tap = UITapGestureRecognizer(target: self, action: #selector(toggleTapped)) addGestureRecognizer(tap) addSubview(row) row.snp.makeConstraints { make in make.edges.equalToSuperview() } checkbox.snp.makeConstraints { make in make.size.equalTo(24) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setChecked(_ checked: Bool) { isChecked = checked checkbox.isSelected = checked } @objc private func toggleTapped() { onToggle?() } } /// 底部确认提交按钮。 final class ScenicApplicationSubmitButton: 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.primaryBanner layer.cornerRadius = 8 snp.makeConstraints { make in make.height.equalTo(46) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(canSubmit: Bool, isSubmitting: Bool, isReadOnly: Bool) { let enabled = !isReadOnly && canSubmit && !isSubmitting isEnabled = enabled alpha = enabled ? 1 : 1 backgroundColor = enabled ? AppColor.primaryBanner : UIColor(hex: 0xE0E0E0) setTitle(isSubmitting ? "提交中..." : "确认提交", for: .normal) } } /// 上传进度弹窗。 final class ScenicApplicationUploadProgressView: UIView { private let titleLabel = UILabel() private let progressView = UIProgressView(progressViewStyle: .default) override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor.black.withAlphaComponent(0.35) let card = UIView() card.backgroundColor = .white card.layer.cornerRadius = 12 titleLabel.font = .systemFont(ofSize: 16, weight: .medium) titleLabel.textColor = AppColor.textPrimary titleLabel.textAlignment = .center progressView.progressTintColor = AppColor.primaryBanner let stack = UIStackView(arrangedSubviews: [titleLabel, progressView]) stack.axis = .vertical stack.spacing = 16 card.addSubview(stack) stack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(20) } addSubview(card) card.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.trailing.equalToSuperview().inset(40) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func apply(state: ScenicApplicationUploadDialogState?) { isHidden = state == nil guard let state else { return } titleLabel.text = state.title progressView.progress = Float(state.progress) / 100 } } /// 带 placeholder 的多行输入框。 final class ScenicApplicationPlaceholderTextView: UITextView, UITextViewDelegate { private let placeholderLabel = UILabel() var placeholder: String { get { placeholderLabel.text ?? "" } set { placeholderLabel.text = newValue refreshPlaceholderVisibility() } } var onTextChange: ((String) -> Void)? override var text: String! { didSet { refreshPlaceholderVisibility() } } override var delegate: UITextViewDelegate? { get { super.delegate } set { // 始终由自身处理 placeholder,外部 delegate 通过 onTextChange 接收变更。 } } init(placeholder: String) { super.init(frame: .zero, textContainer: nil) font = .systemFont(ofSize: 14) textColor = AppColor.textPrimary backgroundColor = .white layer.cornerRadius = 8 layer.borderWidth = 1 layer.borderColor = AppColor.border.cgColor textContainerInset = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 8) super.delegate = self placeholderLabel.text = placeholder placeholderLabel.font = font placeholderLabel.textColor = AppColor.textTertiary placeholderLabel.numberOfLines = 0 addSubview(placeholderLabel) placeholderLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(10) make.leading.equalToSuperview().offset(12) make.trailing.equalToSuperview().offset(-12) } snp.makeConstraints { make in make.height.equalTo(100) } } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func textViewDidChange(_ textView: UITextView) { refreshPlaceholderVisibility() onTextChange?(textView.text ?? "") } private func refreshPlaceholderVisibility() { placeholderLabel.isHidden = !(text ?? "").isEmpty } } /// 带边框的输入框样式。 enum ScenicApplicationFieldStyle { static func makeTextField(placeholder: String) -> UITextField { let field = UITextField() field.placeholder = placeholder field.font = .systemFont(ofSize: 14) field.borderStyle = .none field.backgroundColor = .white 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) } return field } }