Advance UIKit rewrite with AMap integration and core UI modules.

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>
This commit is contained in:
2026-06-26 15:16:12 +08:00
parent 9edf993432
commit d99a5b1bf8
124 changed files with 5195 additions and 1536 deletions

View File

@ -6,6 +6,26 @@
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 {
@ -34,40 +54,55 @@ final class HomeViewController: UIViewController {
return button
}()
private lazy var tableView: UITableView = {
let table = UITableView(frame: .zero, style: .grouped)
table.backgroundColor = AppDesignUIKit.pageBackground
table.separatorStyle = .none
table.showsVerticalScrollIndicator = false
table.dataSource = self
table.delegate = self
table.register(HomeMenuGridCell.self, forCellReuseIdentifier: HomeMenuGridCell.reuseID)
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table
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()
setupTableView()
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
@ -85,20 +120,160 @@ final class HomeViewController: UIViewController {
updateScenicTitle()
}
private func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
/// CollectionView
private func setupCollectionView() {
view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).offset(78)
make.leading.trailing.bottom.equalToSuperview()
}
}
private func bindViewModel() {
viewModel.onChange = { [weak self] in
self?.tableView.reloadData()
/// 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
@ -106,10 +281,11 @@ final class HomeViewController: UIViewController {
}
services.accountContext.onChange = { [weak self] in
self?.updateScenicTitle()
self?.tableView.reloadData()
self?.applySnapshot()
}
}
///
private func rebuildMenus() {
let services = appServices
viewModel.buildMenus(
@ -117,9 +293,10 @@ final class HomeViewController: UIViewController {
currentRoleId: services.permissionContext.currentRole?.id
)
commonURIs = commonMenuStore.load(menuItems: viewModel.menuItems)
tableView.reloadData()
applySnapshot()
}
///
private func updateScenicTitle() {
let name = appServices.accountContext.currentScenic?.name ?? "请选择景区"
scenicButton.configuration?.attributedTitle = AttributedString(
@ -130,25 +307,30 @@ final class HomeViewController: UIViewController {
)
}
/// 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)
@ -160,31 +342,36 @@ final class HomeViewController: UIViewController {
)
}
private var countdownDisplay: String {
let hours = secondsUntilReport / 3_600
let minutes = (secondsUntilReport % 3_600) / 60
let seconds = secondsUntilReport % 60
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
///
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 var reminderText: String {
reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
///
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.tableView.reloadSections(IndexSet(integer: 0), with: .none)
self.applySnapshot(animated: false)
}
}
///
@objc private func scenicTapped() {
HomeMenuRouting.push(.scenicSelection, from: self)
}
/// 线 / 线
@objc private func onlineTapped() {
let message = isOnline
? "是否确认切换为离线状态?离线后将暂停位置上报。"
@ -200,147 +387,137 @@ final class HomeViewController: UIViewController {
} else {
self.countdownTimer?.invalidate()
}
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
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?.tableView.reloadSections(IndexSet(integer: 0), with: .none)
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: - UITableView
// MARK: - UICollectionViewDelegate
extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
var count = 1
if shouldShowWorkStatus { count += 2 }
if isStoreManager, appServices.accountContext.currentStore != nil { count += 1 }
return count
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)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
// MARK: - Card Cells
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == tableView.numberOfSections - 1 {
return "常用应用"
}
return nil
}
/// 线 Cell线
private final class HomeWorkStatusCell: UICollectionViewCell {
static let reuseID = "HomeWorkStatusCell"
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == tableView.numberOfSections - 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: HomeMenuGridCell.reuseID, for: indexPath) as! HomeMenuGridCell
cell.configure(items: displayMenuItems) { [weak self] item in
self.flatMap { HomeMenuRouting.openMenu(item, from: $0) }
}
return cell
}
private let cardView = UIView()
private let onlineButton = UIButton(type: .system)
private let clockLabel = UILabel()
private let reminderButton = UIButton(type: .system)
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.selectionStyle = .none
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 8
contentView.addSubview(cardView)
if shouldShowWorkStatus {
if indexPath.section == 0 {
cell.contentView.addSubview(makeStatusCard())
} else if indexPath.section == 1 {
cell.contentView.addSubview(makeLocationCard())
} else if indexPath.section == 2, isStoreManager, let store = appServices.accountContext.currentStore {
cell.contentView.addSubview(makeStoreCard(store))
} else {
cell.contentView.addSubview(makeQuickActionsRow())
}
} else if isStoreManager, let store = appServices.accountContext.currentStore, indexPath.section == 0 {
cell.contentView.addSubview(makeStoreCard(store))
}
cell.contentView.subviews.first?.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(
top: AppMetrics.Spacing.xxSmall,
left: AppMetrics.Spacing.pageHorizontal,
bottom: AppMetrics.Spacing.xxSmall,
right: AppMetrics.Spacing.pageHorizontal
))
}
cell.backgroundColor = .clear
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == tableView.numberOfSections - 1 { return 240 }
if shouldShowWorkStatus {
switch indexPath.section {
case 0: return 100
case 1: return 148
case 2 where isStoreManager && appServices.accountContext.currentStore != nil: return 88
default: return 118
}
}
if isStoreManager, appServices.accountContext.currentStore != nil, indexPath.section == 0 { return 88 }
return UITableView.automaticDimension
}
private func makeStatusCard() -> UIView {
let card = makeCardView()
let onlineButton = UIButton(type: .system)
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
onlineButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
onlineButton.setTitleColor(isOnline ? UIColor(hex: 0xF0FDF4) : UIColor(hex: 0x7B8EAA), for: .normal)
onlineButton.backgroundColor = isOnline ? UIColor(hex: 0x22C55E) : UIColor(hex: 0xF4F4F4)
onlineButton.layer.cornerRadius = 4
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
let clockLabel = UILabel()
clockLabel.text = " \(countdownDisplay)"
clockLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
clockLabel.textColor = AppDesignUIKit.primary
let reminderButton = UIButton(type: .system)
reminderButton.setTitle(" \(reminderText)", for: .normal)
reminderButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
reminderButton.setTitleColor(AppDesignUIKit.primary, for: .normal)
reminderButton.addTarget(self, action: #selector(reminderTapped), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [onlineButton, clockLabel, reminderButton])
stack.axis = .horizontal
stack.distribution = .equalSpacing
stack.alignment = .center
card.addSubview(stack)
cardView.addSubview(stack)
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
stack.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(15)
}
return card
}
private func makeLocationCard() -> UIView {
let card = makeCardView()
@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 = "立即上报"
@ -356,15 +533,16 @@ extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
textStack.axis = .vertical
textStack.spacing = AppMetrics.Spacing.xxSmall
let actionButton = UIButton(type: .system)
actionButton.setImage(UIImage(systemName: "hand.tap.fill"), for: .normal)
actionButton.tintColor = .white
actionButton.backgroundColor = AppDesignUIKit.primary
actionButton.layer.cornerRadius = 46
actionButton.addTarget(self, action: #selector(locationReportTapped), for: .touchUpInside)
card.addSubview(textStack)
card.addSubview(actionButton)
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)
@ -374,72 +552,38 @@ extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
make.centerY.equalToSuperview()
make.width.height.equalTo(92)
}
return card
}
private func makeQuickActionsRow() -> UIView {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = AppMetrics.Spacing.small
stack.distribution = .fillEqually
stack.addArrangedSubview(quickActionButton(icon: "qrcode", title: "立即收款", action: #selector(paymentTapped)))
stack.addArrangedSubview(quickActionButton(icon: "checklist.checked", title: "提交任务", action: #selector(taskCreateTapped)))
stack.addArrangedSubview(quickActionButton(
icon: isOnline ? "wifi" : "wifi.slash",
title: isOnline ? "在线" : "离线",
action: #selector(onlineTapped),
active: isOnline
))
return stack
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func quickActionButton(icon: String, title: String, action: Selector, active: Bool = false) -> UIView {
let card = makeCardView()
card.backgroundColor = active ? UIColor(hex: 0xE3F2FD) : .white
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 stack = UIStackView(arrangedSubviews: [iconView, label])
stack.axis = .vertical
stack.spacing = AppMetrics.Spacing.xSmall
stack.alignment = .center
card.addSubview(stack)
stack.snp.makeConstraints { make in
make.center.equalToSuperview()
}
iconView.snp.makeConstraints { make in
make.height.equalTo(34)
}
let button = UIButton(type: .custom)
button.addTarget(self, action: 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
///
func configure(onReportTap: @escaping () -> Void) {
actionButton.removeAction(identifiedBy: UIAction.Identifier("report"), for: .touchUpInside)
actionButton.addAction(UIAction(identifier: UIAction.Identifier("report")) { _ in onReportTap() }, for: .touchUpInside)
}
}
private func makeStoreCard(_ store: BusinessScope) -> UIView {
let card = makeCardView()
/// 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)
let nameLabel = UILabel()
nameLabel.text = store.name
nameLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title2, weight: .bold)
let scenicLabel = UILabel()
scenicLabel.text = appServices.accountContext.currentScenic?.name
scenicLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
scenicLabel.textColor = UIColor(hex: 0x7B8EAA)
scenicLabel.numberOfLines = 2
@ -457,8 +601,11 @@ extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
textStack.axis = .vertical
textStack.spacing = AppMetrics.Spacing.xSmall
card.addSubview(textStack)
card.addSubview(statusLabel)
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)
@ -468,39 +615,36 @@ extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
make.width.greaterThanOrEqualTo(52)
make.height.equalTo(22)
}
return card
}
@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
}
}
// MARK: - Menu Grid Cell
/// Cell线
private final class HomeQuickActionsCell: UICollectionViewCell {
static let reuseID = "HomeQuickActionsCell"
private final class HomeMenuGridCell: UITableViewCell {
static let reuseID = "HomeMenuGridCell"
private let stackView = UIStackView()
private var onSelect: ((HomeMenuItem) -> Void)?
private var items: [HomeMenuItem] = []
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 15
layout.minimumLineSpacing = 15
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.isScrollEnabled = false
view.dataSource = self
view.delegate = self
view.register(HomeMenuItemCell.self, forCellWithReuseIdentifier: HomeMenuItemCell.reuseID)
return view
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
///
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
selectionStyle = .none
contentView.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
make.height.equalTo(220)
stackView.axis = .horizontal
stackView.spacing = AppMetrics.Spacing.small
stackView.distribution = .fillEqually
contentView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@ -509,40 +653,87 @@ private final class HomeMenuGridCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}
func configure(items: [HomeMenuItem], onSelect: @escaping (HomeMenuItem) -> Void) {
self.items = items
self.onSelect = onSelect
collectionView.reloadData()
}
}
extension HomeMenuGridCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeMenuItemCell.reuseID, for: indexPath) as! HomeMenuItemCell
cell.configure(item: items[indexPath.item])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
onSelect?(items[indexPath.item])
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.bounds.width - 30) / 3
return CGSize(width: width, height: 102)
///
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
@ -574,6 +765,7 @@ private final class HomeMenuItemCell: UICollectionViewCell {
fatalError("init(coder:) has not been implemented")
}
///
func configure(item: HomeMenuItem) {
titleLabel.text = item.title
let symbol = UIImage(systemName: HomeIconCatalog.iconName(for: item.uri))
@ -581,3 +773,10 @@ private final class HomeMenuItemCell: UICollectionViewCell {
iconView.tintColor = AppDesign.primary
}
}
/// section
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}