// // 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 var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var currentSections: [HomeCollectionSection] = [] 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 collectionView = UICollectionView( frame: .zero, collectionViewLayout: HomeCollectionLayoutBuilder.makeLayout(sections: [.commonApps]) ) collectionView.backgroundColor = .clear collectionView.alwaysBounceVertical = true collectionView.alwaysBounceHorizontal = false collectionView.showsHorizontalScrollIndicator = false collectionView.isDirectionalLockEnabled = true // 水平边距由 section.contentInsets 控制,避免 contentInset 与布局宽度不一致导致横向滑动。 collectionView.contentInset = UIEdgeInsets( top: AppSpacing.sm, left: 0, bottom: AppSpacing.md, right: 0 ) registerCollectionCells() view.addSubview(scenicHeaderView) view.addSubview(collectionView) dataSource = makeDataSource() } override func setupConstraints() { scenicHeaderView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide) make.leading.trailing.equalToSuperview() make.height.equalTo(AppSpacing.homeHeaderHeight) } collectionView.snp.makeConstraints { make in make.top.equalTo(scenicHeaderView.snp.bottom) make.leading.trailing.bottom.equalToSuperview() } } 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() } collectionView.delegate = self 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 viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) } 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 registerCollectionCells() { collectionView.register(HomeWorkStatusCell.self, forCellWithReuseIdentifier: HomeWorkStatusCell.reuseIdentifier) collectionView.register( HomeLocationReportCell.self, forCellWithReuseIdentifier: HomeLocationReportCell.reuseIdentifier ) collectionView.register( HomeQuickActionsCell.self, forCellWithReuseIdentifier: HomeQuickActionsCell.reuseIdentifier ) collectionView.register(HomeStoreCell.self, forCellWithReuseIdentifier: HomeStoreCell.reuseIdentifier) collectionView.register(HomeMenuCell.self, forCellWithReuseIdentifier: HomeMenuCell.reuseIdentifier) collectionView.register( HomeSectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HomeSectionHeaderView.reuseIdentifier ) } private func makeDataSource() -> UICollectionViewDiffableDataSource { let dataSource = UICollectionViewDiffableDataSource( collectionView: collectionView ) { [weak self] collectionView, indexPath, item in guard let self else { return UICollectionViewCell() } let section = self.currentSections[indexPath.section] switch (section, item) { case (.workStatus, .workStatus): let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeWorkStatusCell.reuseIdentifier, for: indexPath ) as! HomeWorkStatusCell cell.cardView.onOnlineTap = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() } cell.cardView.onReminderTap = { [weak self] in self?.presentReminderPicker() } cell.apply( isOnline: self.viewModel.isOnline, countdownText: self.viewModel.countdownDisplayText, reminderMinutes: self.viewModel.reminderMinutes ) return cell case (.locationReport, .locationReport): let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeLocationReportCell.reuseIdentifier, for: indexPath ) as! HomeLocationReportCell cell.cardView.onReportTap = { [weak self] in guard let self else { return } Task { await self.viewModel.manualReportLocation(api: self.homeAPI) } } cell.apply( address: self.viewModel.locationInfoText, isReporting: self.viewModel.isLocationReporting ) return cell case (.quickActions, .quickActions): let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeQuickActionsCell.reuseIdentifier, for: indexPath ) as! HomeQuickActionsCell cell.cardView.onCollectPayment = { [weak self] in guard AppStore.shared.currentScenicId > 0 else { self?.showToast("请先选择景区") return } self?.navigationController?.pushViewController( PaymentCollectionDetailsViewController(), animated: true ) } cell.cardView.onSubmitTask = { [weak self] in self?.navigationController?.pushViewController(TaskAddViewController(), animated: true) } cell.cardView.onToggleOnline = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() } cell.apply(isOnline: self.viewModel.isOnline) return cell case (.store, .store): let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeStoreCell.reuseIdentifier, for: indexPath ) as! HomeStoreCell if let store = self.viewModel.storeItem { cell.apply(store: store) } return cell case (.commonApps, .menu(let menu)): let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeMenuCell.reuseIdentifier, for: indexPath ) as! HomeMenuCell cell.apply(menu: menu) return cell default: return UICollectionViewCell() } } dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in guard kind == UICollectionView.elementKindSectionHeader, indexPath.section < self.currentSections.count, self.currentSections[indexPath.section] == .commonApps else { return nil } let header = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: HomeSectionHeaderView.reuseIdentifier, for: indexPath ) as! HomeSectionHeaderView header.apply(title: "常用应用") return header } return dataSource } 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 let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil let sections = HomeCollectionLayoutBuilder.sectionOrder( showWorkBlock: showWorkBlock, showStore: showStore, storeItem: viewModel.storeItem ) if sections != currentSections { currentSections = sections collectionView.setCollectionViewLayout( HomeCollectionLayoutBuilder.makeLayout(sections: sections), animated: false ) } let snapshot = HomeCollectionLayoutBuilder.makeSnapshot( showWorkBlock: showWorkBlock, showStore: showStore, storeItem: viewModel.storeItem, commonMenus: viewModel.commonMenus ) dataSource.apply(snapshot, animatingDifferences: false) presentDialogsIfNeeded() } private func refreshWorkStatusVisibleCell() { guard !viewModel.isMinimalTopRole, let sectionIndex = currentSections.firstIndex(of: .workStatus) else { return } let indexPath = IndexPath(item: 0, section: sectionIndex) guard let cell = collectionView.cellForItem(at: indexPath) as? HomeWorkStatusCell else { return } cell.apply( isOnline: viewModel.isOnline, countdownText: viewModel.countdownDisplayText, reminderMinutes: viewModel.reminderMinutes ) } 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.isModalInPresentation = true alert.addAction(UIAlertAction(title: "关闭app", style: .default) { _ in UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) }) alert.addAction(UIAlertAction(title: "去入驻", style: .default) { [weak self] _ in self?.activeDialog = nil self?.pushScenicSelection() }) present(alert, animated: true) } private func presentOnlineStatusDialog() { viewModel.hideOnlineStatusSwitchDialog() LocationReportDialogPresenter.presentOnlineStatusSwitch( from: self, isOnline: viewModel.isOnline, onConfirm: { [weak self] in guard let self else { return } self.activeDialog = nil Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) } }, onCancel: { [weak self] in self?.viewModel.hideOnlineStatusSwitchDialog() self?.activeDialog = nil } ) } private func presentLocationTimeoutDialog() { viewModel.hideLocationTimeoutDialog() LocationReportDialogPresenter.presentLocationTimeout( from: self, onReport: { [weak self] in guard let self else { return } self.activeDialog = nil Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) } }, onLater: { [weak self] in self?.viewModel.hideLocationTimeoutDialog() self?.activeDialog = nil } ) } private func presentLocationSuccessDialog() { let reportTime = viewModel.reportTimeText let countdown = viewModel.countdownDisplayText viewModel.hideLocationReportSuccessDialog() LocationReportDialogPresenter.presentReportSuccess( from: self, reportTime: reportTime, countdown: countdown, onDismiss: { [weak self] in self?.activeDialog = nil } ) } private func presentReminderPicker() { LocationReportDialogPresenter.presentReminderPicker(from: self) { [weak self] minutes in self?.viewModel.updateReminderMinutes(minutes) } } private func presentScenicSelection() { pushScenicSelection() } private func pushScenicSelection() { navigationController?.setNavigationBarHidden(false, animated: true) let controller = ScenicSelectionViewController() controller.onSelectionComplete = { [weak self] in guard let self else { return } Task { self.viewModel.refreshLocalDisplayState() self.viewModel.rebuildCommonMenus() await self.viewModel.loadStoreListIfNeeded(api: self.homeAPI) self.applyViewModel() await self.viewModel.evaluateDialogs(api: self.homeAPI) await MainActor.run { self.presentDialogsIfNeeded() } } } navigationController?.pushViewController(controller, 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() self.refreshWorkStatusVisibleCell() } } @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() } } } extension HomeViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard indexPath.section < currentSections.count, currentSections[indexPath.section] == .commonApps, let item = dataSource.itemIdentifier(for: indexPath), case let .menu(menu) = item else { return } HomeRouteHandler.open( uri: menu.uri, from: self, storeItem: viewModel.storeItem, onCommonMenusChanged: { [weak self] in guard let self else { return } self.viewModel.rebuildCommonMenus() self.applyViewModel() } ) } }