完善样片管理列表、详情与上传页 UI 交互。

对齐 Android 样片模块布局与加载状态,补充共享 UI 组件与图片资源,并增强列表解析容错与单元测试覆盖。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 15:00:18 +08:00
parent cbf2db649e
commit afcd412f11
16 changed files with 862 additions and 134 deletions

View File

@ -0,0 +1,155 @@
//
// 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
}
}