375 lines
14 KiB
Swift
375 lines
14 KiB
Swift
//
|
||
// HomeViewController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import SnapKit
|
||
import UIKit
|
||
|
||
/// 首页 Tab 根页面,按身份与权限展示信息与入口。
|
||
final class HomeViewController: BaseViewController {
|
||
|
||
private let viewModel = HomeViewModel()
|
||
private let homeAPI = NetworkServices.shared.homeAPI
|
||
|
||
private let scenicHeaderView = HomeScenicHeaderView()
|
||
private let scrollView = UIScrollView()
|
||
private let contentStack = UIStackView()
|
||
private let workStatusCardView = HomeWorkStatusCardView()
|
||
private let locationReportCardView = HomeLocationReportCardView()
|
||
private let quickActionsView = HomeQuickActionsView()
|
||
private let storeCardView = HomeStoreCardView()
|
||
private let commonAppsGridView = HomeCommonAppsGridView()
|
||
|
||
private var hasInitialized = false
|
||
private var countdownTimer: Timer?
|
||
private var activeDialog: HomeDialogKind?
|
||
|
||
override func setupNavigationBar() {
|
||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||
}
|
||
|
||
override func setupUI() {
|
||
view.backgroundColor = AppColor.pageBackground
|
||
|
||
contentStack.axis = .vertical
|
||
contentStack.spacing = AppSpacing.sm
|
||
|
||
scrollView.showsVerticalScrollIndicator = false
|
||
view.addSubview(scenicHeaderView)
|
||
view.addSubview(scrollView)
|
||
scrollView.addSubview(contentStack)
|
||
|
||
contentStack.addArrangedSubview(workStatusCardView)
|
||
contentStack.addArrangedSubview(locationReportCardView)
|
||
contentStack.addArrangedSubview(quickActionsView)
|
||
contentStack.addArrangedSubview(storeCardView)
|
||
contentStack.addArrangedSubview(commonAppsGridView)
|
||
}
|
||
|
||
override func setupConstraints() {
|
||
scenicHeaderView.snp.makeConstraints { make in
|
||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||
make.leading.trailing.equalToSuperview()
|
||
make.height.equalTo(AppSpacing.homeHeaderHeight)
|
||
}
|
||
scrollView.snp.makeConstraints { make in
|
||
make.top.equalTo(scenicHeaderView.snp.bottom)
|
||
make.leading.trailing.bottom.equalToSuperview()
|
||
}
|
||
contentStack.snp.makeConstraints { make in
|
||
make.edges.equalToSuperview().inset(AppSpacing.screenHorizontalInset)
|
||
make.width.equalTo(scrollView.snp.width).offset(-AppSpacing.screenHorizontalInset * 2)
|
||
}
|
||
}
|
||
|
||
override func bindActions() {
|
||
viewModel.onStateChange = { [weak self] in
|
||
Task { @MainActor in self?.applyViewModel() }
|
||
}
|
||
viewModel.onShowMessage = { [weak self] message in
|
||
Task { @MainActor in self?.showToast(message) }
|
||
}
|
||
|
||
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
||
workStatusCardView.onOnlineTap = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||
workStatusCardView.onReminderTap = { [weak self] in self?.presentReminderPicker() }
|
||
locationReportCardView.onReportTap = { [weak self] in
|
||
Task { await self?.viewModel.manualReportLocation(api: self?.homeAPI ?? NetworkServices.shared.homeAPI) }
|
||
}
|
||
quickActionsView.onCollectPayment = { [weak self] in
|
||
guard AppStore.shared.currentScenicId > 0 else {
|
||
self?.showToast("请先选择景区")
|
||
return
|
||
}
|
||
self?.navigationController?.pushViewController(
|
||
PaymentCollectionDetailsViewController(),
|
||
animated: true
|
||
)
|
||
}
|
||
quickActionsView.onSubmitTask = { [weak self] in self?.showToast("功能开发中") }
|
||
quickActionsView.onToggleOnline = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||
commonAppsGridView.onMenuSelected = { [weak self] menu in
|
||
guard let self else { return }
|
||
HomeRouteHandler.open(uri: menu.uri, from: self, storeItem: self.viewModel.storeItem)
|
||
}
|
||
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleAccountDidSwitch),
|
||
name: NotificationName.accountDidSwitch,
|
||
object: nil
|
||
)
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleScenicDidChange),
|
||
name: NotificationName.scenicDidChange,
|
||
object: nil
|
||
)
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
if !hasInitialized {
|
||
hasInitialized = true
|
||
Task { await initializeHome() }
|
||
} else {
|
||
Task { await viewModel.reloadIfNeeded(api: homeAPI) }
|
||
}
|
||
startCountdownTimer()
|
||
}
|
||
|
||
override func viewWillDisappear(_ animated: Bool) {
|
||
super.viewWillDisappear(animated)
|
||
countdownTimer?.invalidate()
|
||
countdownTimer = nil
|
||
}
|
||
|
||
deinit {
|
||
NotificationCenter.default.removeObserver(self)
|
||
}
|
||
|
||
private func initializeHome() async {
|
||
showLoading()
|
||
await viewModel.initialize(api: homeAPI)
|
||
hideLoading()
|
||
|
||
applyViewModel()
|
||
await evaluateDialogsWithDelay()
|
||
await viewModel.refreshLocationInfo()
|
||
applyViewModel()
|
||
}
|
||
|
||
private func evaluateDialogsWithDelay() async {
|
||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||
await viewModel.evaluateDialogs(api: homeAPI)
|
||
applyViewModel()
|
||
presentDialogsIfNeeded()
|
||
}
|
||
|
||
@MainActor
|
||
private func applyViewModel() {
|
||
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
|
||
let showWorkBlock = !viewModel.isMinimalTopRole
|
||
workStatusCardView.isHidden = !showWorkBlock
|
||
locationReportCardView.isHidden = !showWorkBlock
|
||
quickActionsView.isHidden = !showWorkBlock
|
||
|
||
if showWorkBlock {
|
||
workStatusCardView.apply(
|
||
isOnline: viewModel.isOnline,
|
||
countdownText: viewModel.countdownDisplayText,
|
||
reminderMinutes: viewModel.reminderMinutes
|
||
)
|
||
locationReportCardView.apply(
|
||
address: viewModel.locationInfoText,
|
||
isReporting: viewModel.isLocationReporting
|
||
)
|
||
quickActionsView.apply(isOnline: viewModel.isOnline)
|
||
}
|
||
|
||
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
||
storeCardView.isHidden = !showStore
|
||
if showStore, let store = viewModel.storeItem {
|
||
storeCardView.apply(store: store)
|
||
}
|
||
|
||
commonAppsGridView.apply(menus: viewModel.commonMenus)
|
||
presentDialogsIfNeeded()
|
||
}
|
||
|
||
private func presentDialogsIfNeeded() {
|
||
if viewModel.showPermissionDialog {
|
||
presentDialog(.permission)
|
||
return
|
||
}
|
||
if viewModel.showScenicDialog {
|
||
presentDialog(.scenic)
|
||
return
|
||
}
|
||
if viewModel.showOnlineStatusDialog {
|
||
presentDialog(.onlineStatus)
|
||
return
|
||
}
|
||
if viewModel.showLocationTimeoutDialog {
|
||
presentDialog(.locationTimeout)
|
||
return
|
||
}
|
||
if viewModel.showLocationReportSuccessDialog {
|
||
presentDialog(.locationSuccess)
|
||
return
|
||
}
|
||
activeDialog = nil
|
||
}
|
||
|
||
private enum HomeDialogKind {
|
||
case permission
|
||
case scenic
|
||
case onlineStatus
|
||
case locationTimeout
|
||
case locationSuccess
|
||
}
|
||
|
||
private func presentDialog(_ kind: HomeDialogKind) {
|
||
guard activeDialog != kind, presentedViewController == nil else { return }
|
||
activeDialog = kind
|
||
switch kind {
|
||
case .permission: presentPermissionDialog()
|
||
case .scenic: presentScenicDialog()
|
||
case .onlineStatus: presentOnlineStatusDialog()
|
||
case .locationTimeout: presentLocationTimeoutDialog()
|
||
case .locationSuccess: presentLocationSuccessDialog()
|
||
}
|
||
}
|
||
|
||
private func presentPermissionDialog() {
|
||
let alert = UIAlertController(
|
||
title: "权限提示",
|
||
message: "您还没有权限,请先申请权限",
|
||
preferredStyle: .alert
|
||
)
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||
self?.viewModel.hidePermissionDialog()
|
||
self?.activeDialog = nil
|
||
})
|
||
alert.addAction(UIAlertAction(title: "去申请", style: .default) { [weak self] _ in
|
||
self?.showToast("功能开发中")
|
||
self?.activeDialog = nil
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentScenicDialog() {
|
||
let alert = UIAlertController(
|
||
title: "请选择景区",
|
||
message: "使用首页功能前需要先选择当前景区",
|
||
preferredStyle: .alert
|
||
)
|
||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||
self?.viewModel.hideScenicDialog()
|
||
self?.activeDialog = nil
|
||
})
|
||
alert.addAction(UIAlertAction(title: "去选择", style: .default) { [weak self] _ in
|
||
self?.viewModel.hideScenicDialog()
|
||
self?.activeDialog = nil
|
||
self?.presentScenicSelection()
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentOnlineStatusDialog() {
|
||
let goingOnline = !viewModel.isOnline
|
||
let alert = UIAlertController(
|
||
title: goingOnline ? "切换在线" : "切换离线",
|
||
message: goingOnline ? "确认切换到在线状态并上报位置?" : "确认切换到离线状态?",
|
||
preferredStyle: .alert
|
||
)
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||
self?.viewModel.hideOnlineStatusSwitchDialog()
|
||
self?.activeDialog = nil
|
||
})
|
||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
self.activeDialog = nil
|
||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentLocationTimeoutDialog() {
|
||
let alert = UIAlertController(
|
||
title: "位置上报提醒",
|
||
message: "您的位置上报即将超时,请立即上报",
|
||
preferredStyle: .alert
|
||
)
|
||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||
self?.viewModel.hideLocationTimeoutDialog()
|
||
self?.activeDialog = nil
|
||
})
|
||
alert.addAction(UIAlertAction(title: "立即上报", style: .default) { [weak self] _ in
|
||
guard let self else { return }
|
||
self.activeDialog = nil
|
||
Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) }
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentLocationSuccessDialog() {
|
||
let alert = UIAlertController(
|
||
title: "上报成功",
|
||
message: "上报时间:\(viewModel.reportTimeText)",
|
||
preferredStyle: .alert
|
||
)
|
||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||
self?.viewModel.hideLocationReportSuccessDialog()
|
||
self?.activeDialog = nil
|
||
})
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentReminderPicker() {
|
||
let alert = UIAlertController(title: "提前提醒", message: "选择位置超时提前提醒时间", preferredStyle: .actionSheet)
|
||
[0, 5, 10, 15, 30].forEach { minutes in
|
||
let title = minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
|
||
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||
self?.viewModel.updateReminderMinutes(minutes)
|
||
})
|
||
}
|
||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||
present(alert, animated: true)
|
||
}
|
||
|
||
private func presentScenicSelection() {
|
||
let scenicList = AppStore.shared.roleScenicList()
|
||
guard !scenicList.isEmpty else {
|
||
showToast("暂无可选景区")
|
||
return
|
||
}
|
||
let controller = ScenicSelectionViewController(scenicList: scenicList)
|
||
controller.onSelected = { [weak self] in
|
||
guard let self else { return }
|
||
Task {
|
||
await self.viewModel.reloadIfNeeded(api: self.homeAPI)
|
||
await self.viewModel.loadStoreListIfNeeded(api: self.homeAPI)
|
||
self.applyViewModel()
|
||
await self.evaluateDialogsWithDelay()
|
||
}
|
||
}
|
||
let nav = UINavigationController(rootViewController: controller)
|
||
nav.modalPresentationStyle = .pageSheet
|
||
present(nav, animated: true)
|
||
}
|
||
|
||
private func startCountdownTimer() {
|
||
countdownTimer?.invalidate()
|
||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||
guard let self else { return }
|
||
self.viewModel.triggerLocationTimeoutIfNeeded()
|
||
if !self.viewModel.isMinimalTopRole {
|
||
self.workStatusCardView.apply(
|
||
isOnline: self.viewModel.isOnline,
|
||
countdownText: self.viewModel.countdownDisplayText,
|
||
reminderMinutes: self.viewModel.reminderMinutes
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc private func handleAccountDidSwitch() {
|
||
viewModel.markNeedsPermissionReload()
|
||
Task {
|
||
await viewModel.reloadIfNeeded(api: homeAPI)
|
||
applyViewModel()
|
||
await evaluateDialogsWithDelay()
|
||
}
|
||
}
|
||
|
||
@objc private func handleScenicDidChange() {
|
||
viewModel.refreshLocalDisplayState()
|
||
Task {
|
||
await viewModel.loadStoreListIfNeeded(api: homeAPI)
|
||
applyViewModel()
|
||
}
|
||
}
|
||
}
|