完善首页权限申请流程与队列播报体验。
对齐 Android 无权限强制弹窗、角色下拉与申请页交互,补充 UIButton configuration 适配,并增强景区排队 TTS 与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,11 +6,26 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 权限申请页橙色横幅(跳转景区申请)。
|
||||
/// 权限申请模块视觉常量,严格对应 Android Compose 中的尺寸和颜色。
|
||||
enum PermissionApplyUITokens {
|
||||
static let pageBackground = UIColor(hex: 0xF5F5F5)
|
||||
static let selectorBorder = UIColor(hex: 0xE0E3EA)
|
||||
static let placeholder = UIColor(hex: 0xB3B8C2)
|
||||
static let text = UIColor(hex: 0x111827)
|
||||
static let existingBackground = UIColor(hex: 0xF8F9FB)
|
||||
static let selectedBackground = UIColor(hex: 0xF4F9FF)
|
||||
static let selectedBlue = UIColor(hex: 0x1677FF)
|
||||
static let existingGreen = UIColor(hex: 0x52C41A)
|
||||
static let existingGreenBackground = UIColor(hex: 0xE6F7E6)
|
||||
static let disabledButton = UIColor(hex: 0xE0E0E0)
|
||||
}
|
||||
|
||||
/// 权限申请页顶部“申请平台开通新景区”横幅。
|
||||
final class PermissionApplyBannerView: UIView {
|
||||
|
||||
var onApplyNewScenic: (() -> Void)?
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let applyButton = UIButton(type: .system)
|
||||
|
||||
@ -18,28 +33,50 @@ final class PermissionApplyBannerView: UIView {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = AppColor.warningBackground
|
||||
|
||||
iconView.image = UIImage(named: "permission_open_scenic")
|
||||
?? UIImage(systemName: "building.2.fill")?.withTintColor(AppColor.warning, renderingMode: .alwaysOriginal)
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.isAccessibilityElement = false
|
||||
|
||||
titleLabel.text = "没有找到心仪的景区"
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.warning
|
||||
titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
applyButton.setTitle("申请平台开通新景区", for: .normal)
|
||||
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
applyButton.setTitleColor(AppColor.warning, for: .normal)
|
||||
var configuration = UIButton.Configuration.plain()
|
||||
configuration.title = "申请平台开通新景区"
|
||||
configuration.image = UIImage(named: "permission_more_scenic")
|
||||
?? UIImage(systemName: "chevron.right")
|
||||
configuration.imagePlacement = .trailing
|
||||
configuration.imagePadding = 2
|
||||
configuration.contentInsets = .zero
|
||||
configuration.baseForegroundColor = AppColor.warning
|
||||
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
||||
var outgoing = incoming
|
||||
outgoing.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
return outgoing
|
||||
}
|
||||
applyButton.configuration = configuration
|
||||
applyButton.accessibilityLabel = "申请平台开通新景区"
|
||||
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(applyButton)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
[iconView, titleLabel, applyButton].forEach(addSubview)
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(20)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(applyButton.snp.leading).offset(-12)
|
||||
}
|
||||
applyButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.height.greaterThanOrEqualTo(44)
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
snp.makeConstraints { make in make.height.equalTo(48) }
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -52,7 +89,7 @@ final class PermissionApplyBannerView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 表单字段选择行,对齐 Android `FormField` + Selector。
|
||||
/// Android 表单标题、说明和锚定选择框组合视图。
|
||||
final class PermissionFormSelectorRow: UIView {
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
@ -60,6 +97,7 @@ final class PermissionFormSelectorRow: UIView {
|
||||
private let titleLabel = UILabel()
|
||||
private let tagLabel = UILabel()
|
||||
private let descriptionLabel = UILabel()
|
||||
let selectorControl = UIControl()
|
||||
private let valueLabel = UILabel()
|
||||
private let chevron = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
|
||||
@ -67,7 +105,7 @@ final class PermissionFormSelectorRow: UIView {
|
||||
super.init(frame: .zero)
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||||
|
||||
tagLabel.text = tag
|
||||
tagLabel.font = .systemFont(ofSize: 12)
|
||||
@ -76,28 +114,28 @@ final class PermissionFormSelectorRow: UIView {
|
||||
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.font = .systemFont(ofSize: 12)
|
||||
descriptionLabel.textColor = UIColor(hex: 0xB3B8C2)
|
||||
descriptionLabel.textColor = PermissionApplyUITokens.placeholder
|
||||
descriptionLabel.numberOfLines = 0
|
||||
|
||||
selectorControl.backgroundColor = .white
|
||||
selectorControl.layer.cornerRadius = 12
|
||||
selectorControl.layer.borderWidth = 1
|
||||
selectorControl.layer.borderColor = PermissionApplyUITokens.selectorBorder.cgColor
|
||||
selectorControl.accessibilityLabel = title
|
||||
selectorControl.addTarget(self, action: #selector(rowTapped), for: .touchUpInside)
|
||||
|
||||
valueLabel.text = placeholder
|
||||
valueLabel.font = .systemFont(ofSize: 14)
|
||||
valueLabel.textColor = UIColor(hex: 0xB3B8C2)
|
||||
valueLabel.textColor = PermissionApplyUITokens.placeholder
|
||||
valueLabel.lineBreakMode = .byTruncatingTail
|
||||
|
||||
chevron.tintColor = UIColor(hex: 0xB3B8C2)
|
||||
chevron.tintColor = PermissionApplyUITokens.placeholder
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped))
|
||||
addGestureRecognizer(tap)
|
||||
[titleLabel, tagLabel, descriptionLabel, selectorControl].forEach(addSubview)
|
||||
[valueLabel, chevron].forEach(selectorControl.addSubview)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(tagLabel)
|
||||
addSubview(descriptionLabel)
|
||||
addSubview(valueLabel)
|
||||
addSubview(chevron)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview()
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in make.top.leading.equalToSuperview() }
|
||||
tagLabel.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(titleLabel)
|
||||
make.trailing.equalToSuperview()
|
||||
@ -107,121 +145,18 @@ final class PermissionFormSelectorRow: UIView {
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
selectorControl.snp.makeConstraints { make in
|
||||
make.top.equalTo(descriptionLabel.snp.bottom).offset(8)
|
||||
make.leading.bottom.equalToSuperview()
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.equalTo(46)
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.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 ? UIColor(hex: 0xB3B8C2) : 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: 16, weight: .medium)
|
||||
titleLabel.textColor = UIColor(hex: 0x111827)
|
||||
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.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(20)
|
||||
}
|
||||
@ -232,23 +167,156 @@ final class PermissionTagChip: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func removeTapped() {
|
||||
onRemove?()
|
||||
/// 更新选择框显示值。
|
||||
func setValue(_ text: String, isPlaceholder: Bool) {
|
||||
valueLabel.text = text
|
||||
valueLabel.textColor = isPlaceholder ? PermissionApplyUITokens.placeholder : PermissionApplyUITokens.text
|
||||
selectorControl.accessibilityValue = text
|
||||
}
|
||||
|
||||
@objc private func rowTapped() {
|
||||
onTap?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部主按钮。
|
||||
final class PermissionSubmitButton: UIButton {
|
||||
/// 权限标签的展示样式。
|
||||
enum PermissionChipStyle: Hashable {
|
||||
case selectedRemovable
|
||||
case selectedStatic
|
||||
case existingGreen
|
||||
}
|
||||
|
||||
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)
|
||||
/// Diffable 标签网格中的稳定条目。
|
||||
struct PermissionChipItem: Hashable {
|
||||
let id: String
|
||||
let title: String
|
||||
}
|
||||
|
||||
/// Android `FlowRow` 的标签左对齐与换行坐标计算器。
|
||||
struct PermissionChipLayoutMetrics {
|
||||
static let itemHeight: CGFloat = 44
|
||||
static let interitemSpacing: CGFloat = 12
|
||||
static let lineSpacing: CGFloat = 2
|
||||
|
||||
/// 按容器宽度为标签生成从左到右、超宽换行的坐标。
|
||||
static func makeFrames(itemWidths: [CGFloat], containerWidth: CGFloat) -> [CGRect] {
|
||||
guard containerWidth > 0 else { return [] }
|
||||
var x: CGFloat = 0
|
||||
var y: CGFloat = 0
|
||||
return itemWidths.map { rawWidth in
|
||||
let width = min(containerWidth, max(44, rawWidth))
|
||||
if x > 0, x + width > containerWidth + 0.5 {
|
||||
x = 0
|
||||
y += itemHeight + lineSpacing
|
||||
}
|
||||
let frame = CGRect(x: x, y: y, width: width, height: itemHeight)
|
||||
x += width + interitemSpacing
|
||||
return frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 确定性计算左对齐换行坐标,等价于 Android `FlowRow`。
|
||||
private final class PermissionChipFlowLayout: UICollectionViewLayout {
|
||||
|
||||
var itemWidthProvider: ((IndexPath) -> CGFloat)?
|
||||
private var cachedAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:]
|
||||
private var contentHeight: CGFloat = 1
|
||||
|
||||
override func prepare() {
|
||||
super.prepare()
|
||||
guard let collectionView, collectionView.bounds.width > 0 else {
|
||||
cachedAttributes = [:]
|
||||
contentHeight = 1
|
||||
return
|
||||
}
|
||||
|
||||
cachedAttributes = [:]
|
||||
let availableWidth = collectionView.bounds.width
|
||||
var indexPaths: [IndexPath] = []
|
||||
var itemWidths: [CGFloat] = []
|
||||
for section in 0..<collectionView.numberOfSections {
|
||||
for item in 0..<collectionView.numberOfItems(inSection: section) {
|
||||
let indexPath = IndexPath(item: item, section: section)
|
||||
indexPaths.append(indexPath)
|
||||
itemWidths.append(itemWidthProvider?(indexPath) ?? 44)
|
||||
}
|
||||
}
|
||||
let frames = PermissionChipLayoutMetrics.makeFrames(
|
||||
itemWidths: itemWidths,
|
||||
containerWidth: availableWidth
|
||||
)
|
||||
for (index, indexPath) in indexPaths.enumerated() {
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
attributes.frame = frames[index]
|
||||
cachedAttributes[indexPath] = attributes
|
||||
}
|
||||
contentHeight = frames.last?.maxY ?? 1
|
||||
}
|
||||
|
||||
override var collectionViewContentSize: CGSize {
|
||||
CGSize(width: collectionView?.bounds.width ?? 0, height: contentHeight)
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
cachedAttributes.values.filter { $0.frame.intersects(rect) }
|
||||
}
|
||||
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
cachedAttributes[indexPath]
|
||||
}
|
||||
|
||||
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
guard let collectionView else { return true }
|
||||
return abs(collectionView.bounds.width - newBounds.width) > 0.5
|
||||
}
|
||||
}
|
||||
|
||||
/// 自适应换行的权限标签网格。
|
||||
final class PermissionChipCollectionView: UIView {
|
||||
|
||||
private enum Section { case main }
|
||||
|
||||
var onRemove: ((Int) -> Void)?
|
||||
|
||||
private let style: PermissionChipStyle
|
||||
private let layout: PermissionChipFlowLayout
|
||||
private let collectionView: UICollectionView
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, PermissionChipItem>!
|
||||
private var items: [PermissionChipItem] = []
|
||||
private var heightConstraint: Constraint?
|
||||
|
||||
init(style: PermissionChipStyle) {
|
||||
self.style = style
|
||||
let layout = PermissionChipFlowLayout()
|
||||
self.layout = layout
|
||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
super.init(frame: .zero)
|
||||
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.isScrollEnabled = false
|
||||
collectionView.register(PermissionChipCell.self, forCellWithReuseIdentifier: PermissionChipCell.reuseIdentifier)
|
||||
addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() }
|
||||
snp.makeConstraints { make in heightConstraint = make.height.equalTo(1).constraint }
|
||||
|
||||
dataSource = UICollectionViewDiffableDataSource<Section, PermissionChipItem>(
|
||||
collectionView: collectionView
|
||||
) { [weak self] collectionView, indexPath, item in
|
||||
guard let self,
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: PermissionChipCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as? PermissionChipCell else { return UICollectionViewCell() }
|
||||
cell.apply(title: item.title, style: self.style)
|
||||
cell.onRemove = { [weak self] in
|
||||
guard let self, let currentIndex = self.items.firstIndex(of: item) else { return }
|
||||
self.onRemove?(currentIndex)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
layout.itemWidthProvider = { [weak self] indexPath in
|
||||
self?.itemWidth(at: indexPath) ?? 44
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,9 +325,189 @@ final class PermissionSubmitButton: UIButton {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setSubmitting(_ submitting: Bool) {
|
||||
isEnabled = !submitting
|
||||
alpha = submitting ? 0.6 : 1
|
||||
setTitle(submitting ? "提交中..." : "提交审核", for: .normal)
|
||||
/// 使用 Diffable snapshot 更新标签。
|
||||
func apply(titles: [String]) {
|
||||
var occurrences: [String: Int] = [:]
|
||||
items = titles.map { title in
|
||||
let occurrence = occurrences[title, default: 0]
|
||||
occurrences[title] = occurrence + 1
|
||||
return PermissionChipItem(id: "\(title)#\(occurrence)", title: title)
|
||||
}
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, PermissionChipItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
|
||||
self?.updateHeight()
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateHeight()
|
||||
}
|
||||
|
||||
private func updateHeight() {
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
collectionView.layoutIfNeeded()
|
||||
let height = max(1, collectionView.collectionViewLayout.collectionViewContentSize.height)
|
||||
heightConstraint?.update(offset: height)
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
private func itemWidth(at indexPath: IndexPath) -> CGFloat {
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else { return 44 }
|
||||
let textWidth = ceil((item.title as NSString).size(withAttributes: [
|
||||
.font: UIFont.systemFont(ofSize: 14),
|
||||
]).width)
|
||||
let horizontalChrome: CGFloat = style == .selectedRemovable ? 42 : 24
|
||||
return textWidth + horizontalChrome
|
||||
}
|
||||
}
|
||||
|
||||
/// 带标题背景的“新增角色/新增景区”区域。
|
||||
final class PermissionSelectedTagsView: UIView {
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let chips = PermissionChipCollectionView(style: .selectedRemovable)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = PermissionApplyUITokens.selectedBackground
|
||||
layer.cornerRadius = 12
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = PermissionApplyUITokens.text
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(chips)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(12)
|
||||
}
|
||||
chips.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
isHidden = names.isEmpty
|
||||
chips.onRemove = onRemove
|
||||
chips.apply(titles: names)
|
||||
}
|
||||
}
|
||||
|
||||
/// 权限申请页底部主按钮。
|
||||
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)
|
||||
layer.cornerRadius = 8
|
||||
accessibilityLabel = "提交审核"
|
||||
snp.makeConstraints { make in make.height.equalTo(48) }
|
||||
applyState(canSubmit: false, submitting: false)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新可提交与提交中状态。
|
||||
func applyState(canSubmit: Bool, submitting: Bool) {
|
||||
isEnabled = canSubmit && !submitting
|
||||
backgroundColor = isEnabled ? PermissionApplyUITokens.selectedBlue : PermissionApplyUITokens.disabledButton
|
||||
setTitle(submitting ? "提交中..." : "提交审核", for: .normal)
|
||||
accessibilityValue = submitting ? "提交中" : nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 权限标签 CollectionView 单元格。
|
||||
private final class PermissionChipCell: UICollectionViewCell {
|
||||
|
||||
static let reuseIdentifier = "PermissionChipCell"
|
||||
|
||||
var onRemove: (() -> Void)?
|
||||
|
||||
private let chipBackground = UIView()
|
||||
private let label = UILabel()
|
||||
private let removeButton = UIButton(type: .system)
|
||||
private let removeImageView = UIImageView(image: UIImage(systemName: "xmark"))
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
removeButton.addTarget(self, action: #selector(removeTapped), for: .touchUpInside)
|
||||
removeButton.accessibilityLabel = "删除"
|
||||
removeImageView.contentMode = .scaleAspectFit
|
||||
removeImageView.isUserInteractionEnabled = false
|
||||
contentView.addSubview(chipBackground)
|
||||
chipBackground.addSubview(label)
|
||||
chipBackground.addSubview(removeImageView)
|
||||
contentView.addSubview(removeButton)
|
||||
|
||||
chipBackground.snp.makeConstraints { make in
|
||||
make.leading.trailing.centerY.equalToSuperview()
|
||||
make.height.equalTo(34)
|
||||
}
|
||||
removeButton.snp.makeConstraints { make in
|
||||
make.trailing.centerY.equalToSuperview()
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
removeImageView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-10)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(14)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(title: String, style: PermissionChipStyle) {
|
||||
label.text = title
|
||||
chipBackground.layer.cornerRadius = style == .existingGreen ? 4 : 8
|
||||
chipBackground.layer.borderWidth = 1
|
||||
switch style {
|
||||
case .selectedRemovable, .selectedStatic:
|
||||
chipBackground.backgroundColor = style == .selectedRemovable
|
||||
? PermissionApplyUITokens.selectedBackground : .white
|
||||
chipBackground.layer.borderColor = PermissionApplyUITokens.selectedBlue.cgColor
|
||||
label.textColor = PermissionApplyUITokens.selectedBlue
|
||||
removeImageView.tintColor = PermissionApplyUITokens.selectedBlue
|
||||
case .existingGreen:
|
||||
chipBackground.backgroundColor = PermissionApplyUITokens.existingGreenBackground
|
||||
chipBackground.layer.borderColor = PermissionApplyUITokens.existingGreen.cgColor
|
||||
label.textColor = PermissionApplyUITokens.existingGreen
|
||||
removeImageView.tintColor = PermissionApplyUITokens.existingGreen
|
||||
}
|
||||
let removable = style == .selectedRemovable
|
||||
removeButton.isHidden = !removable
|
||||
removeImageView.isHidden = !removable
|
||||
label.snp.remakeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
if removable {
|
||||
make.trailing.equalTo(removeImageView.snp.leading).offset(-6)
|
||||
} else {
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
}
|
||||
}
|
||||
accessibilityLabel = title
|
||||
}
|
||||
|
||||
@objc private func removeTapped() {
|
||||
onRemove?()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user