// // 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.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 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 } }