Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. 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() {
|
||
HomeMenuRouting.push(.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
|
||
}
|
||
}
|