Implement permission-driven home tab aligned with Android.

Load role-permission on first visit to drive menus, role-based layout, store card, and location reporting with account-scoped persistence and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 17:27:54 +08:00
parent 75d0cb1f9a
commit 3b17b7f7f0
27 changed files with 2573 additions and 10 deletions

View File

@ -0,0 +1,132 @@
//
// HomeCommonAppsGridView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HomeCommonAppsGridView: UIView {
var onMenuSelected: ((HomeMenuItem) -> Void)?
private enum Section { case main }
private enum Item: Hashable { case menu(HomeMenuItem) }
private var menus: [HomeMenuItem] = []
private let titleLabel = UILabel()
private lazy var collectionView: UICollectionView = {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.25),
heightDimension: .absolute(88)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(88)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
return UICollectionView(frame: .zero, collectionViewLayout: layout)
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
collectionView, indexPath, item in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: HomeMenuCell.reuseIdentifier,
for: indexPath
) as! HomeMenuCell
if case let .menu(menu) = item {
cell.apply(menu: menu)
}
return cell
}
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "常用应用"
titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
titleLabel.textColor = AppColor.text333
collectionView.backgroundColor = .clear
collectionView.delegate = self
collectionView.register(HomeMenuCell.self, forCellWithReuseIdentifier: HomeMenuCell.reuseIdentifier)
addSubview(titleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
collectionView.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(88)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(menus: [HomeMenuItem]) {
self.menus = menus + [.moreFunctions]
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(self.menus.map { Item.menu($0) })
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension HomeCommonAppsGridView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard indexPath.item < menus.count else { return }
onMenuSelected?(menus[indexPath.item])
}
}
/// cell
final class HomeMenuCell: UICollectionViewCell {
static let reuseIdentifier = "HomeMenuCell"
private let iconView = UIImageView()
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
iconView.tintColor = AppColor.primary
iconView.contentMode = .scaleAspectFit
titleLabel.font = .systemFont(ofSize: 12)
titleLabel.textColor = AppColor.text333
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 2
contentView.addSubview(iconView)
contentView.addSubview(titleLabel)
iconView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(8)
make.centerX.equalToSuperview()
make.width.height.equalTo(28)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconView.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(4)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(menu: HomeMenuItem) {
iconView.image = UIImage(systemName: menu.iconName)
titleLabel.text = menu.title
}
}

View File

@ -0,0 +1,70 @@
//
// HomeLocationReportCardView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HomeLocationReportCardView: UIView {
var onReportTap: (() -> Void)?
private let titleLabel = UILabel()
private let addressLabel = UILabel()
private let reportButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 10
titleLabel.text = "位置上报"
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
titleLabel.textColor = AppColor.text333
addressLabel.font = .systemFont(ofSize: 13)
addressLabel.textColor = AppColor.text666
addressLabel.numberOfLines = 2
reportButton.setTitle("立即上报", for: .normal)
reportButton.setTitleColor(.white, for: .normal)
reportButton.backgroundColor = AppColor.primary
reportButton.layer.cornerRadius = 6
reportButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
reportButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 14, bottom: 8, right: 14)
reportButton.addTarget(self, action: #selector(reportTapped), for: .touchUpInside)
addSubview(titleLabel)
addSubview(addressLabel)
addSubview(reportButton)
titleLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(15)
}
addressLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.leading.equalToSuperview().offset(15)
make.bottom.equalToSuperview().offset(-15)
make.trailing.lessThanOrEqualTo(reportButton.snp.leading).offset(-12)
}
reportButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-15)
make.centerY.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(address: String, isReporting: Bool) {
addressLabel.text = address
reportButton.isEnabled = !isReporting
reportButton.alpha = isReporting ? 0.6 : 1
}
@objc private func reportTapped() { onReportTap?() }
}

View File

@ -0,0 +1,86 @@
//
// HomeQuickActionsView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HomeQuickActionsView: UIView {
var onCollectPayment: (() -> Void)?
var onSubmitTask: (() -> Void)?
var onToggleOnline: (() -> Void)?
private let stackView = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 12
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(72)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(isOnline: Bool) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
stackView.addArrangedSubview(makeActionButton(title: "立即收款", icon: "yensign.circle", action: #selector(collectPaymentTapped)))
stackView.addArrangedSubview(makeActionButton(title: "提交任务", icon: "doc.text", action: #selector(submitTaskTapped)))
stackView.addArrangedSubview(makeActionButton(
title: isOnline ? "在线" : "离线",
icon: isOnline ? "dot.radiowaves.left.and.right" : "moon",
action: #selector(toggleOnlineTapped)
))
}
private func makeActionButton(title: String, icon: String, action: Selector) -> UIView {
let container = UIView()
container.backgroundColor = .white
container.layer.cornerRadius = 10
let button = UIButton(type: .system)
button.addTarget(self, action: action, for: .touchUpInside)
container.addSubview(button)
button.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
let imageView = UIImageView(image: UIImage(systemName: icon))
imageView.tintColor = AppColor.primary
imageView.contentMode = .scaleAspectFit
let label = UILabel()
label.text = title
label.font = .systemFont(ofSize: 13, weight: .medium)
label.textColor = AppColor.text333
label.textAlignment = .center
let column = UIStackView(arrangedSubviews: [imageView, label])
column.axis = .vertical
column.alignment = .center
column.spacing = 6
column.isUserInteractionEnabled = false
container.addSubview(column)
column.snp.makeConstraints { make in
make.center.equalToSuperview()
}
imageView.snp.makeConstraints { make in
make.width.height.equalTo(22)
}
return container
}
@objc private func collectPaymentTapped() { onCollectPayment?() }
@objc private func submitTaskTapped() { onSubmitTask?() }
@objc private func toggleOnlineTapped() { onToggleOnline?() }
}

View File

@ -0,0 +1,56 @@
//
// HomeScenicHeaderView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HomeScenicHeaderView: UIView {
var onTap: (() -> Void)?
private let titleLabel = UILabel()
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
titleLabel.textColor = AppColor.text333
arrowView.tintColor = AppColor.text666
arrowView.contentMode = .scaleAspectFit
addSubview(titleLabel)
addSubview(arrowView)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(16)
make.centerY.equalToSuperview()
make.trailing.lessThanOrEqualTo(arrowView.snp.leading).offset(-8)
}
arrowView.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-16)
make.centerY.equalToSuperview()
make.width.height.equalTo(16)
}
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tap)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(scenicName: String) {
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
}
@objc private func handleTap() {
onTap?()
}
}

View File

@ -0,0 +1,60 @@
//
// HomeStoreCardView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HomeStoreCardView: UIView {
private let nameLabel = UILabel()
private let addressLabel = UILabel()
private let statusLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 10
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
nameLabel.textColor = AppColor.text333
addressLabel.font = .systemFont(ofSize: 13)
addressLabel.textColor = AppColor.text666
addressLabel.numberOfLines = 2
statusLabel.font = .systemFont(ofSize: 12, weight: .medium)
statusLabel.textColor = AppColor.primary
addSubview(nameLabel)
addSubview(addressLabel)
addSubview(statusLabel)
nameLabel.snp.makeConstraints { make in
make.top.leading.equalToSuperview().offset(15)
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-8)
}
statusLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(15)
make.trailing.equalToSuperview().offset(-15)
}
addressLabel.snp.makeConstraints { make in
make.top.equalTo(nameLabel.snp.bottom).offset(8)
make.leading.trailing.equalToSuperview().inset(15)
make.bottom.equalToSuperview().offset(-15)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(store: StoreItem) {
nameLabel.text = store.name
addressLabel.text = store.address
statusLabel.text = store.statusText.isEmpty ? "营业中" : store.statusText
}
}

View File

@ -0,0 +1,77 @@
//
// HomeWorkStatusCardView.swift
// suixinkan
//
import SnapKit
import UIKit
/// 线
final class HomeWorkStatusCardView: UIView {
var onOnlineTap: (() -> Void)?
var onReminderTap: (() -> Void)?
private let onlineButton = UIButton(type: .system)
private let countdownLabel = UILabel()
private let reminderButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 10
onlineButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
onlineButton.layer.cornerRadius = 4
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
countdownLabel.font = .systemFont(ofSize: 16, weight: .medium)
countdownLabel.textColor = AppColor.primary
reminderButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
reminderButton.setTitleColor(AppColor.primary, for: .normal)
reminderButton.addTarget(self, action: #selector(reminderTapped), for: .touchUpInside)
addSubview(onlineButton)
addSubview(countdownLabel)
addSubview(reminderButton)
onlineButton.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(15)
make.centerY.equalToSuperview()
}
countdownLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
reminderButton.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-15)
make.centerY.equalToSuperview()
}
snp.makeConstraints { make in
make.height.equalTo(52)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(isOnline: Bool, countdownText: String, reminderMinutes: Int) {
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
if isOnline {
onlineButton.backgroundColor = UIColor(hex: 0x22C55E)
onlineButton.setTitleColor(UIColor(hex: 0xF0FDF4), for: .normal)
} else {
onlineButton.backgroundColor = UIColor(hex: 0xF4F4F4)
onlineButton.setTitleColor(UIColor(hex: 0x7B8EAA), for: .normal)
}
countdownLabel.text = countdownText
let reminderTitle = reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
reminderButton.setTitle(reminderTitle, for: .normal)
}
@objc private func onlineTapped() { onOnlineTap?() }
@objc private func reminderTapped() { onReminderTap?() }
}