// // 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 = [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! /// 视图加载完成后的 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( 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() 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 } }