Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
783 lines
30 KiB
Swift
783 lines
30 KiB
Swift
//
|
||
// HomeViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
// MARK: - Diffable 标识
|
||
|
||
/// 首页 section 标识,按角色动态展示工作状态、门店与常用应用网格。
|
||
private enum HomeSection: Hashable {
|
||
case workStatus
|
||
case locationReport
|
||
case storeInfo
|
||
case quickActions
|
||
case commonMenus
|
||
}
|
||
|
||
/// 首页 item 标识;工作状态类 item 携带展示数据以支持倒计时 diff 刷新。
|
||
private enum HomeItem: Hashable {
|
||
case workStatus(isOnline: Bool, secondsUntilReport: Int, reminderMinutes: Int)
|
||
case locationReport
|
||
case storeInfo(storeID: Int)
|
||
case quickActions(isOnline: Bool)
|
||
case menu(uri: String)
|
||
}
|
||
|
||
/// 首页工作台,展示景区头部、工作状态、位置上报卡片和常用应用网格。
|
||
final class HomeViewController: UIViewController {
|
||
|
||
private let viewModel = HomeViewModel()
|
||
private let commonMenuStore = HomeCommonMenuStore()
|
||
private var commonURIs: [String] = []
|
||
private var isOnline = false
|
||
private var reminderMinutes = 0
|
||
private var secondsUntilReport = 0
|
||
private var countdownTimer: Timer?
|
||
|
||
private let minimalTopRoleIds: Set<Int> = [46, 47, 52, 53, 54]
|
||
|
||
private lazy var scenicButton: UIButton = {
|
||
var config = UIButton.Configuration.plain()
|
||
config.baseForegroundColor = UIColor(hex: 0x333333)
|
||
config.image = UIImage(systemName: "chevron.down")?.withConfiguration(
|
||
UIImage.SymbolConfiguration(pointSize: AppMetrics.FontSize.subheadline, weight: .bold)
|
||
)
|
||
config.imagePlacement = .trailing
|
||
config.imagePadding = AppMetrics.Spacing.xSmall
|
||
config.contentInsets = .zero
|
||
let button = UIButton(configuration: config)
|
||
button.contentHorizontalAlignment = .leading
|
||
button.addTarget(self, action: #selector(scenicTapped), for: .touchUpInside)
|
||
return button
|
||
}()
|
||
|
||
private lazy var collectionView: UICollectionView = {
|
||
let layout = makeCollectionLayout()
|
||
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||
collection.backgroundColor = AppDesignUIKit.pageBackground
|
||
collection.showsVerticalScrollIndicator = false
|
||
collection.delegate = self
|
||
collection.register(HomeWorkStatusCell.self, forCellWithReuseIdentifier: HomeWorkStatusCell.reuseID)
|
||
collection.register(HomeLocationReportCell.self, forCellWithReuseIdentifier: HomeLocationReportCell.reuseID)
|
||
collection.register(HomeStoreInfoCell.self, forCellWithReuseIdentifier: HomeStoreInfoCell.reuseID)
|
||
collection.register(HomeQuickActionsCell.self, forCellWithReuseIdentifier: HomeQuickActionsCell.reuseID)
|
||
collection.register(HomeMenuItemCell.self, forCellWithReuseIdentifier: HomeMenuItemCell.reuseID)
|
||
collection.register(
|
||
CollectionSectionHeaderView.self,
|
||
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
|
||
withReuseIdentifier: CollectionSectionHeaderView.reuseID
|
||
)
|
||
return collection
|
||
}()
|
||
|
||
/// Diffable 数据源,驱动多 section 卡片与常用应用网格。
|
||
private var dataSource: UICollectionViewDiffableDataSource<HomeSection, HomeItem>!
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||
setupTopBar()
|
||
configureDataSource()
|
||
setupCollectionView()
|
||
bindViewModel()
|
||
rebuildMenus()
|
||
observeContextChanges()
|
||
}
|
||
|
||
/// 视图即将展示,刷新可见状态。
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||
startCountdownTimerIfNeeded()
|
||
}
|
||
|
||
/// 视图即将消失,保存或清理临时状态。
|
||
override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
countdownTimer?.invalidate()
|
||
countdownTimer = nil
|
||
}
|
||
|
||
/// 初始化 TopBar 相关 UI 或状态。
|
||
private func setupTopBar() {
|
||
let topBar = UIView()
|
||
topBar.backgroundColor = .white
|
||
view.addSubview(topBar)
|
||
topBar.addSubview(scenicButton)
|
||
|
||
topBar.snp.makeConstraints { make in
|
||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||
make.height.equalTo(78)
|
||
}
|
||
scenicButton.snp.makeConstraints { make in
|
||
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
|
||
make.centerY.equalToSuperview()
|
||
}
|
||
updateScenicTitle()
|
||
}
|
||
|
||
/// 初始化 CollectionView 布局约束。
|
||
private func setupCollectionView() {
|
||
view.addSubview(collectionView)
|
||
collectionView.snp.makeConstraints { make in
|
||
make.top.equalTo(view.safeAreaLayoutGuide).offset(78)
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
/// 构建 Compositional Layout,按 section 类型分配全宽卡片或三列网格。
|
||
private func makeCollectionLayout() -> UICollectionViewCompositionalLayout {
|
||
UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
|
||
guard let self,
|
||
let section = self.dataSource?.snapshot().sectionIdentifiers[safe: sectionIndex]
|
||
else {
|
||
return CollectionDiffableLayout.fullWidthSection()
|
||
}
|
||
switch section {
|
||
case .workStatus:
|
||
return CollectionDiffableLayout.fullWidthSection(height: 100)
|
||
case .locationReport:
|
||
return CollectionDiffableLayout.fullWidthSection(height: 148)
|
||
case .storeInfo:
|
||
return CollectionDiffableLayout.fullWidthSection(height: 88)
|
||
case .quickActions:
|
||
return CollectionDiffableLayout.fullWidthSection(height: 118)
|
||
case .commonMenus:
|
||
return CollectionDiffableLayout.addHeader(
|
||
to: CollectionDiffableLayout.gridSection(itemHeight: 102),
|
||
height: 36
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 注册 Diffable 数据源与 Cell 配置闭包。
|
||
private func configureDataSource() {
|
||
dataSource = UICollectionViewDiffableDataSource<HomeSection, HomeItem>(
|
||
collectionView: collectionView
|
||
) { [weak self] collectionView, indexPath, item in
|
||
guard let self else { return UICollectionViewCell() }
|
||
switch item {
|
||
case .workStatus(let online, let seconds, let reminder):
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: HomeWorkStatusCell.reuseID,
|
||
for: indexPath
|
||
) as! HomeWorkStatusCell
|
||
cell.configure(
|
||
isOnline: online,
|
||
countdownText: self.countdownText(seconds: seconds),
|
||
reminderText: self.reminderText(minutes: reminder),
|
||
onOnlineTap: { [weak self] in self?.onlineTapped() },
|
||
onReminderTap: { [weak self] in self?.reminderTapped() }
|
||
)
|
||
return cell
|
||
case .locationReport:
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: HomeLocationReportCell.reuseID,
|
||
for: indexPath
|
||
) as! HomeLocationReportCell
|
||
cell.configure(onReportTap: { [weak self] in self?.locationReportTapped() })
|
||
return cell
|
||
case .storeInfo(let storeID):
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: HomeStoreInfoCell.reuseID,
|
||
for: indexPath
|
||
) as! HomeStoreInfoCell
|
||
if let store = self.appServices.accountContext.currentStore, store.id == storeID {
|
||
cell.configure(
|
||
storeName: store.name,
|
||
scenicName: self.appServices.accountContext.currentScenic?.name
|
||
)
|
||
}
|
||
return cell
|
||
case .quickActions(let online):
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: HomeQuickActionsCell.reuseID,
|
||
for: indexPath
|
||
) as! HomeQuickActionsCell
|
||
cell.configure(
|
||
isOnline: online,
|
||
onPaymentTap: { [weak self] in self?.paymentTapped() },
|
||
onTaskCreateTap: { [weak self] in self?.taskCreateTapped() },
|
||
onOnlineTap: { [weak self] in self?.onlineTapped() }
|
||
)
|
||
return cell
|
||
case .menu(let uri):
|
||
let cell = collectionView.dequeueReusableCell(
|
||
withReuseIdentifier: HomeMenuItemCell.reuseID,
|
||
for: indexPath
|
||
) as! HomeMenuItemCell
|
||
if let menuItem = self.menuItem(for: uri) ?? self.displayMenuItems.first(where: { $0.uri == uri }) {
|
||
cell.configure(item: menuItem)
|
||
}
|
||
return cell
|
||
}
|
||
}
|
||
|
||
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
|
||
guard let self,
|
||
kind == UICollectionView.elementKindSectionHeader,
|
||
let section = self.dataSource.snapshot().sectionIdentifiers[safe: indexPath.section],
|
||
section == .commonMenus,
|
||
let header = collectionView.dequeueReusableSupplementaryView(
|
||
ofKind: kind,
|
||
withReuseIdentifier: CollectionSectionHeaderView.reuseID,
|
||
for: indexPath
|
||
) as? CollectionSectionHeaderView
|
||
else { return nil }
|
||
header.configure(title: "常用应用")
|
||
return header
|
||
}
|
||
}
|
||
|
||
/// 根据当前角色与状态构建 snapshot 并应用 diff 更新。
|
||
private func applySnapshot(animated: Bool = true) {
|
||
var snapshot = NSDiffableDataSourceSnapshot<HomeSection, HomeItem>()
|
||
|
||
if shouldShowWorkStatus {
|
||
snapshot.appendSections([.workStatus, .locationReport])
|
||
snapshot.appendItems(
|
||
[.workStatus(isOnline: isOnline, secondsUntilReport: secondsUntilReport, reminderMinutes: reminderMinutes)],
|
||
toSection: .workStatus
|
||
)
|
||
snapshot.appendItems([.locationReport], toSection: .locationReport)
|
||
}
|
||
|
||
if isStoreManager, let store = appServices.accountContext.currentStore {
|
||
snapshot.appendSections([.storeInfo])
|
||
snapshot.appendItems([.storeInfo(storeID: store.id)], toSection: .storeInfo)
|
||
}
|
||
|
||
if shouldShowWorkStatus {
|
||
snapshot.appendSections([.quickActions])
|
||
snapshot.appendItems([.quickActions(isOnline: isOnline)], toSection: .quickActions)
|
||
}
|
||
|
||
snapshot.appendSections([.commonMenus])
|
||
snapshot.appendItems(
|
||
displayMenuItems.map { HomeItem.menu(uri: $0.uri) },
|
||
toSection: .commonMenus
|
||
)
|
||
|
||
dataSource.apply(snapshot, animatingDifferences: animated)
|
||
}
|
||
|
||
/// 绑定 ViewModel 回调或数据。
|
||
private func bindViewModel() {
|
||
viewModel.onChange = { [weak self] in
|
||
self?.applySnapshot()
|
||
}
|
||
}
|
||
|
||
/// 监听权限与账号上下文变化并刷新菜单。
|
||
private func observeContextChanges() {
|
||
let services = appServices
|
||
services.permissionContext.onChange = { [weak self] in
|
||
self?.rebuildMenus()
|
||
}
|
||
services.accountContext.onChange = { [weak self] in
|
||
self?.updateScenicTitle()
|
||
self?.applySnapshot()
|
||
}
|
||
}
|
||
|
||
/// 按当前角色权限重建首页菜单与常用应用。
|
||
private func rebuildMenus() {
|
||
let services = appServices
|
||
viewModel.buildMenus(
|
||
from: services.permissionContext.rolePermissions,
|
||
currentRoleId: services.permissionContext.currentRole?.id
|
||
)
|
||
commonURIs = commonMenuStore.load(menuItems: viewModel.menuItems)
|
||
applySnapshot()
|
||
}
|
||
|
||
/// 刷新顶部景区名称展示。
|
||
private func updateScenicTitle() {
|
||
let name = appServices.accountContext.currentScenic?.name ?? "请选择景区"
|
||
scenicButton.configuration?.attributedTitle = AttributedString(
|
||
name,
|
||
attributes: AttributeContainer([
|
||
.font: UIFont.systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
|
||
])
|
||
)
|
||
}
|
||
|
||
/// 当前登录角色 ID。
|
||
private var currentRoleId: Int? {
|
||
appServices.permissionContext.currentRole?.id
|
||
}
|
||
|
||
/// 部分精简角色不展示工作状态与位置上报区块。
|
||
private var shouldShowWorkStatus: Bool {
|
||
guard let currentRoleId else { return true }
|
||
return !minimalTopRoleIds.contains(currentRoleId)
|
||
}
|
||
|
||
/// 是否为门店管理员角色(roleId = 46)。
|
||
private var isStoreManager: Bool {
|
||
currentRoleId == 46
|
||
}
|
||
|
||
/// 常用应用展示项:优先用户自定义,不足 3 个时取权限菜单前 3 项,末尾固定「更多功能」。
|
||
private var displayMenuItems: [HomeMenuItem] {
|
||
let selected = commonURIs.compactMap { menuItem(for: $0) }
|
||
let fallback = selected.isEmpty ? Array(viewModel.menuItems.prefix(3)) : selected
|
||
return fallback + [HomeMenuItem(title: "更多功能", uri: "more_functions", iconSrc: nil)]
|
||
}
|
||
|
||
/// 按 URI 解析并返回可用菜单项。
|
||
private func menuItem(for uri: String) -> HomeMenuItem? {
|
||
let availableURIs = Set(viewModel.menuItems.map(\.uri))
|
||
let resolvedUri = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
|
||
guard let existing = viewModel.menuItems.first(where: { $0.uri == resolvedUri }) else { return nil }
|
||
return HomeMenuItem(
|
||
title: HomeMenuRouter.displayTitle(for: resolvedUri, fallback: existing.title),
|
||
uri: resolvedUri,
|
||
iconSrc: existing.iconSrc
|
||
)
|
||
}
|
||
|
||
/// 格式化倒计时展示文本。
|
||
private func countdownText(seconds: Int) -> String {
|
||
let hours = seconds / 3_600
|
||
let minutes = (seconds % 3_600) / 60
|
||
let secs = seconds % 60
|
||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", secs))"
|
||
}
|
||
|
||
/// 格式化提前提醒展示文本。
|
||
private func reminderText(minutes: Int) -> String {
|
||
minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
|
||
}
|
||
|
||
/// 在线状态下启动位置上报倒计时。
|
||
private func startCountdownTimerIfNeeded() {
|
||
countdownTimer?.invalidate()
|
||
guard isOnline else { return }
|
||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||
guard let self, self.isOnline, self.secondsUntilReport > 0 else { return }
|
||
self.secondsUntilReport -= 1
|
||
self.applySnapshot(animated: false)
|
||
}
|
||
}
|
||
|
||
/// 点击景区名称,跳转景区选择页。
|
||
@objc private func scenicTapped() {
|
||
appServices.appNavigator.push(.home(.scenicSelection), from: self)
|
||
}
|
||
|
||
/// 切换在线 / 离线状态。
|
||
@objc private func onlineTapped() {
|
||
let message = isOnline
|
||
? "是否确认切换为离线状态?离线后将暂停位置上报。"
|
||
: "是否确认切换为在线状态?在线后将开始位置上报和计时。"
|
||
let alert = UIAlertController(title: "切换在线状态", message: message, preferredStyle: .alert)
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
self.isOnline.toggle()
|
||
if self.isOnline {
|
||
self.secondsUntilReport = 7_200
|
||
self.startCountdownTimerIfNeeded()
|
||
} else {
|
||
self.countdownTimer?.invalidate()
|
||
}
|
||
self.applySnapshot()
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
/// 选择位置上报提前提醒时间。
|
||
@objc private func reminderTapped() {
|
||
let sheet = UIAlertController(title: "提前提醒时间", message: nil, preferredStyle: .actionSheet)
|
||
for minute in [0, 5, 10, 15, 30] {
|
||
let title = minute == 0 ? "不提醒" : "\(minute)分钟"
|
||
sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||
self?.reminderMinutes = minute
|
||
self?.applySnapshot()
|
||
})
|
||
}
|
||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(sheet, animated: true)
|
||
}
|
||
|
||
/// 跳转位置上报页面。
|
||
@objc private func locationReportTapped() {
|
||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "location_report", title: "位置上报"), from: self)
|
||
}
|
||
|
||
/// 跳转立即收款页面。
|
||
@objc private func paymentTapped() {
|
||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "payment_collection", title: "立即收款"), from: self)
|
||
}
|
||
|
||
/// 跳转提交任务页面。
|
||
@objc private func taskCreateTapped() {
|
||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "task_create", title: "提交任务"), from: self)
|
||
}
|
||
}
|
||
|
||
// MARK: - UICollectionViewDelegate
|
||
|
||
extension HomeViewController: UICollectionViewDelegate {
|
||
/// 点击常用应用网格项,执行菜单路由。
|
||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||
guard let item = dataSource.itemIdentifier(for: indexPath),
|
||
case .menu(let uri) = item,
|
||
let menuItem = displayMenuItems.first(where: { $0.uri == uri })
|
||
else { return }
|
||
HomeMenuRouting.openMenu(menuItem, from: self)
|
||
}
|
||
}
|
||
|
||
// MARK: - Card Cells
|
||
|
||
/// 在线状态卡片 Cell,展示在线切换、倒计时与提醒设置。
|
||
private final class HomeWorkStatusCell: UICollectionViewCell {
|
||
static let reuseID = "HomeWorkStatusCell"
|
||
|
||
private let cardView = UIView()
|
||
private let onlineButton = UIButton(type: .system)
|
||
private let clockLabel = UILabel()
|
||
private let reminderButton = UIButton(type: .system)
|
||
|
||
/// 初始化实例。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
cardView.backgroundColor = .white
|
||
cardView.layer.cornerRadius = 8
|
||
contentView.addSubview(cardView)
|
||
|
||
onlineButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
|
||
onlineButton.layer.cornerRadius = 4
|
||
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
|
||
|
||
clockLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||
clockLabel.textColor = AppDesignUIKit.primary
|
||
|
||
reminderButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||
reminderButton.setTitleColor(AppDesignUIKit.primary, for: .normal)
|
||
|
||
let stack = UIStackView(arrangedSubviews: [onlineButton, clockLabel, reminderButton])
|
||
stack.axis = .horizontal
|
||
stack.distribution = .equalSpacing
|
||
stack.alignment = .center
|
||
cardView.addSubview(stack)
|
||
|
||
cardView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
stack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(15)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 配置在线状态、倒计时与提醒,并绑定点击回调。
|
||
func configure(
|
||
isOnline: Bool,
|
||
countdownText: String,
|
||
reminderText: String,
|
||
onOnlineTap: @escaping () -> Void,
|
||
onReminderTap: @escaping () -> Void
|
||
) {
|
||
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
|
||
onlineButton.setTitleColor(isOnline ? UIColor(hex: 0xF0FDF4) : UIColor(hex: 0x7B8EAA), for: .normal)
|
||
onlineButton.backgroundColor = isOnline ? UIColor(hex: 0x22C55E) : UIColor(hex: 0xF4F4F4)
|
||
clockLabel.text = " \(countdownText)"
|
||
reminderButton.setTitle(" \(reminderText)", for: .normal)
|
||
|
||
onlineButton.removeAction(identifiedBy: UIAction.Identifier("online"), for: .touchUpInside)
|
||
reminderButton.removeAction(identifiedBy: UIAction.Identifier("reminder"), for: .touchUpInside)
|
||
onlineButton.addAction(UIAction(identifier: UIAction.Identifier("online")) { _ in onOnlineTap() }, for: .touchUpInside)
|
||
reminderButton.addAction(UIAction(identifier: UIAction.Identifier("reminder")) { _ in onReminderTap() }, for: .touchUpInside)
|
||
}
|
||
}
|
||
|
||
/// 位置上报卡片 Cell,展示打卡范围提示与立即上报入口。
|
||
private final class HomeLocationReportCell: UICollectionViewCell {
|
||
static let reuseID = "HomeLocationReportCell"
|
||
|
||
private let cardView = UIView()
|
||
private let actionButton = UIButton(type: .system)
|
||
|
||
/// 初始化实例。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
cardView.backgroundColor = .white
|
||
cardView.layer.cornerRadius = 8
|
||
contentView.addSubview(cardView)
|
||
|
||
let titleLabel = UILabel()
|
||
titleLabel.text = "立即上报"
|
||
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||
|
||
let subtitleLabel = UILabel()
|
||
subtitleLabel.text = "您已进入打卡范围"
|
||
subtitleLabel.font = .systemFont(ofSize: 15)
|
||
subtitleLabel.textColor = UIColor(hex: 0x999999)
|
||
|
||
let textStack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||
textStack.axis = .vertical
|
||
textStack.spacing = AppMetrics.Spacing.xxSmall
|
||
|
||
actionButton.setImage(UIImage(systemName: "hand.tap.fill"), for: .normal)
|
||
actionButton.tintColor = .white
|
||
actionButton.backgroundColor = AppDesignUIKit.primary
|
||
actionButton.layer.cornerRadius = 46
|
||
|
||
cardView.addSubview(textStack)
|
||
cardView.addSubview(actionButton)
|
||
cardView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
textStack.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(15)
|
||
make.trailing.lessThanOrEqualTo(actionButton.snp.leading).offset(-12)
|
||
}
|
||
actionButton.snp.makeConstraints { make in
|
||
make.trailing.equalToSuperview().inset(15)
|
||
make.centerY.equalToSuperview()
|
||
make.width.height.equalTo(92)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 配置立即上报按钮点击回调。
|
||
func configure(onReportTap: @escaping () -> Void) {
|
||
actionButton.removeAction(identifiedBy: UIAction.Identifier("report"), for: .touchUpInside)
|
||
actionButton.addAction(UIAction(identifier: UIAction.Identifier("report")) { _ in onReportTap() }, for: .touchUpInside)
|
||
}
|
||
}
|
||
|
||
/// 门店信息卡片 Cell,展示当前门店名称与营业状态。
|
||
private final class HomeStoreInfoCell: UICollectionViewCell {
|
||
static let reuseID = "HomeStoreInfoCell"
|
||
|
||
private let cardView = UIView()
|
||
private let nameLabel = UILabel()
|
||
private let scenicLabel = UILabel()
|
||
|
||
/// 初始化实例。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
cardView.backgroundColor = .white
|
||
cardView.layer.cornerRadius = 8
|
||
contentView.addSubview(cardView)
|
||
|
||
nameLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title2, weight: .bold)
|
||
|
||
scenicLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||
scenicLabel.textColor = UIColor(hex: 0x7B8EAA)
|
||
scenicLabel.numberOfLines = 2
|
||
|
||
let statusLabel = UILabel()
|
||
statusLabel.text = "营业中"
|
||
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||
statusLabel.textColor = UIColor(hex: 0x22C55E)
|
||
statusLabel.backgroundColor = UIColor(hex: 0xF0FDF4)
|
||
statusLabel.layer.cornerRadius = 4
|
||
statusLabel.clipsToBounds = true
|
||
statusLabel.textAlignment = .center
|
||
|
||
let textStack = UIStackView(arrangedSubviews: [nameLabel, scenicLabel])
|
||
textStack.axis = .vertical
|
||
textStack.spacing = AppMetrics.Spacing.xSmall
|
||
|
||
cardView.addSubview(textStack)
|
||
cardView.addSubview(statusLabel)
|
||
cardView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
textStack.snp.makeConstraints { make in
|
||
make.leading.top.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-8)
|
||
}
|
||
statusLabel.snp.makeConstraints { make in
|
||
make.trailing.top.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||
make.width.greaterThanOrEqualTo(52)
|
||
make.height.equalTo(22)
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 配置门店名称与所属景区。
|
||
func configure(storeName: String, scenicName: String?) {
|
||
nameLabel.text = storeName
|
||
scenicLabel.text = scenicName
|
||
}
|
||
}
|
||
|
||
/// 快捷操作行 Cell,展示收款、任务与在线状态入口。
|
||
private final class HomeQuickActionsCell: UICollectionViewCell {
|
||
static let reuseID = "HomeQuickActionsCell"
|
||
|
||
private let stackView = UIStackView()
|
||
|
||
/// 初始化实例。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
stackView.axis = .horizontal
|
||
stackView.spacing = AppMetrics.Spacing.small
|
||
stackView.distribution = .fillEqually
|
||
contentView.addSubview(stackView)
|
||
stackView.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 配置三个快捷操作按钮与点击回调。
|
||
func configure(
|
||
isOnline: Bool,
|
||
onPaymentTap: @escaping () -> Void,
|
||
onTaskCreateTap: @escaping () -> Void,
|
||
onOnlineTap: @escaping () -> Void
|
||
) {
|
||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
stackView.addArrangedSubview(quickActionCard(
|
||
icon: "qrcode",
|
||
title: "立即收款",
|
||
active: false,
|
||
action: onPaymentTap
|
||
))
|
||
stackView.addArrangedSubview(quickActionCard(
|
||
icon: "checklist.checked",
|
||
title: "提交任务",
|
||
active: false,
|
||
action: onTaskCreateTap
|
||
))
|
||
stackView.addArrangedSubview(quickActionCard(
|
||
icon: isOnline ? "wifi" : "wifi.slash",
|
||
title: isOnline ? "在线" : "离线",
|
||
active: isOnline,
|
||
action: onOnlineTap
|
||
))
|
||
}
|
||
|
||
/// 构建单个快捷操作卡片。
|
||
private func quickActionCard(
|
||
icon: String,
|
||
title: String,
|
||
active: Bool,
|
||
action: @escaping () -> Void
|
||
) -> UIView {
|
||
let card = UIView()
|
||
card.backgroundColor = active ? UIColor(hex: 0xE3F2FD) : .white
|
||
card.layer.cornerRadius = 8
|
||
|
||
let iconView = UIImageView(image: UIImage(systemName: icon))
|
||
iconView.tintColor = AppDesignUIKit.primary
|
||
iconView.contentMode = .scaleAspectFit
|
||
|
||
let label = UILabel()
|
||
label.text = title
|
||
label.font = .systemFont(ofSize: AppMetrics.FontSize.body)
|
||
label.textAlignment = .center
|
||
|
||
let innerStack = UIStackView(arrangedSubviews: [iconView, label])
|
||
innerStack.axis = .vertical
|
||
innerStack.spacing = AppMetrics.Spacing.xSmall
|
||
innerStack.alignment = .center
|
||
card.addSubview(innerStack)
|
||
innerStack.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
}
|
||
iconView.snp.makeConstraints { make in
|
||
make.height.equalTo(34)
|
||
}
|
||
|
||
let button = UIButton(type: .custom)
|
||
button.addAction(UIAction { _ in action() }, for: .touchUpInside)
|
||
card.addSubview(button)
|
||
button.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview()
|
||
}
|
||
card.snp.makeConstraints { make in
|
||
make.height.equalTo(102)
|
||
}
|
||
return card
|
||
}
|
||
}
|
||
|
||
/// 首页菜单网格单项 Cell,负责图标与标题展示。
|
||
private final class HomeMenuItemCell: UICollectionViewCell {
|
||
static let reuseID = "HomeMenuItemCell"
|
||
|
||
private let iconView = UIImageView()
|
||
private let titleLabel = UILabel()
|
||
|
||
/// 初始化实例。
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
contentView.backgroundColor = .white
|
||
contentView.layer.cornerRadius = 8
|
||
|
||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body)
|
||
titleLabel.textColor = UIColor(hex: 0x4B5563)
|
||
titleLabel.textAlignment = .center
|
||
titleLabel.numberOfLines = 1
|
||
titleLabel.adjustsFontSizeToFitWidth = true
|
||
|
||
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel])
|
||
stack.axis = .vertical
|
||
stack.spacing = AppMetrics.Spacing.small
|
||
stack.alignment = .center
|
||
contentView.addSubview(stack)
|
||
stack.snp.makeConstraints { make in
|
||
make.center.equalToSuperview()
|
||
make.leading.trailing.equalToSuperview().inset(4)
|
||
}
|
||
iconView.snp.makeConstraints { make in
|
||
make.width.height.equalTo(30)
|
||
}
|
||
iconView.contentMode = .scaleAspectFit
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 配置菜单项图标与标题。
|
||
func configure(item: HomeMenuItem) {
|
||
titleLabel.text = item.title
|
||
let symbol = UIImage(systemName: HomeIconCatalog.iconName(for: item.uri))
|
||
iconView.loadRemoteImage(urlString: item.iconSrc, contentMode: .scaleAspectFit, placeholder: symbol)
|
||
iconView.tintColor = AppDesign.primary
|
||
}
|
||
}
|
||
|
||
/// 安全下标,避免 section 越界。
|
||
private extension Array {
|
||
subscript(safe index: Int) -> Element? {
|
||
indices.contains(index) ? self[index] : nil
|
||
}
|
||
}
|