Files
lujiuyinandCursor efb3925257 完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:46:45 +08:00

158 lines
5.7 KiB
Swift

//
// SampleManagementUI.swift
// suixinkan
//
import SnapKit
import UIKit
/// 样片管理模块视觉常量,严格对应 Android Compose 页面中的 dp、颜色与圆角。
enum SampleManagementUITokens {
static let pageBackground = UIColor(hex: 0xF4F4F4)
static let inputBackground = UIColor(hex: 0xF4F4F4)
static let border = UIColor(hex: 0xEEEEEE)
static let placeholder = UIColor(hex: 0xB6BECA)
static let text563 = UIColor(hex: 0x4B5563)
static let statText = UIColor(hex: 0x6B7280)
static let screenInset: CGFloat = 16
static let sectionSpacing: CGFloat = 12
static let cardRadius: CGFloat = 12
static let fieldRadius: CGFloat = 8
}
/// 样片管理页面基类,复用“空间设置”的原生导航栏外观与返回行为。
class SampleBaseViewController: BaseViewController {
override func setupNavigationBar() {
navigationItem.backButtonDisplayMode = .minimal
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
ProfileNavigationStyle.apply(to: navigationController, showsDivider: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
ProfileNavigationStyle.restore(navigationController)
}
/// 设置与“空间设置”一致的原生导航栏标题。
func setSampleTitle(_ title: String) {
self.title = title
}
}
/// 锚定在输入框下方的 Android 风格下拉菜单,支持滚动加载更多。
final class SampleAnchoredDropdownView: UIControl, UIScrollViewDelegate {
/// 下拉菜单选项。
struct Option {
let title: String
let isSelected: Bool
let isLoadMore: Bool
/// 创建普通或“加载更多”选项。
init(title: String, isSelected: Bool = false, isLoadMore: Bool = false) {
self.title = title
self.isSelected = isSelected
self.isLoadMore = isLoadMore
}
}
private let cardView = UIView()
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private var onSelect: ((Int) -> Void)?
private var onReachedBottom: (() -> Void)?
private var didRequestBottom = false
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 4
cardView.layer.shadowColor = UIColor.black.cgColor
cardView.layer.shadowOpacity = 0.14
cardView.layer.shadowRadius = 8
cardView.layer.shadowOffset = CGSize(width: 0, height: 4)
scrollView.delegate = self
stackView.axis = .vertical
addSubview(cardView)
cardView.addSubview(scrollView)
scrollView.addSubview(stackView)
addTarget(self, action: #selector(dismissMenu), for: .touchUpInside)
isHidden = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 在锚点下方展示菜单。
func show(
anchor: UIView,
options: [Option],
maximumHeight: CGFloat = 200,
onReachedBottom: (() -> Void)? = nil,
onSelect: @escaping (Int) -> Void
) {
guard !options.isEmpty else { return }
self.onSelect = onSelect
self.onReachedBottom = onReachedBottom
didRequestBottom = false
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for (index, option) in options.enumerated() {
let button = UIButton(type: .system)
button.tag = index
button.contentHorizontalAlignment = .leading
button.setConfigurationContentInsets(
NSDirectionalEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12)
)
button.setTitle(option.title, for: .normal)
let color = option.isSelected ? AppColor.primary : (option.isLoadMore ? AppColor.textSecondary : .black)
button.setTitleColor(color, for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 14, weight: option.isSelected ? .medium : .regular)
button.backgroundColor = .white
button.accessibilityLabel = option.title
button.addTarget(self, action: #selector(optionTapped(_:)), for: .touchUpInside)
button.snp.makeConstraints { make in make.height.equalTo(28) }
stackView.addArrangedSubview(button)
}
isHidden = false
superview?.bringSubviewToFront(self)
layoutIfNeeded()
let anchorFrame = anchor.convert(anchor.bounds, to: self)
let contentHeight = CGFloat(options.count) * 28
let menuHeight = min(maximumHeight, contentHeight)
cardView.frame = CGRect(x: anchorFrame.minX, y: anchorFrame.maxY, width: anchorFrame.width, height: menuHeight)
scrollView.frame = cardView.bounds
stackView.frame = CGRect(x: 0, y: 0, width: cardView.bounds.width, height: contentHeight)
scrollView.contentSize = stackView.frame.size
}
/// 隐藏菜单。
func hide() {
isHidden = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !didRequestBottom, scrollView.contentSize.height > scrollView.bounds.height else { return }
let remaining = scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.bounds.height
guard remaining <= 10 else { return }
didRequestBottom = true
onReachedBottom?()
}
@objc private func optionTapped(_ sender: UIButton) {
isHidden = true
onSelect?(sender.tag)
}
@objc private func dismissMenu() {
isHidden = true
}
}