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:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user