// // HomeMoreFunctionsViewController.swift // suixinkan // import SnapKit import UIKit // MARK: - Diffable 标识 /// 全部功能页 section 标识,区分常用应用与更多功能网格。 private enum HomeMoreSection: Hashable { case commonApps case moreFunctions } /// 全部功能页 item 标识,以菜单 URI 作为唯一键。 private enum HomeMoreItem: Hashable { case menu(uri: String) } /// 首页全部功能页,支持常用应用增删和更多功能网格展示。 final class HomeMoreFunctionsViewController: UIViewController { private let viewModel = HomeViewModel() private let commonMenuStore = HomeCommonMenuStore() private var commonURIs: [String] = [] private lazy var collectionView: UICollectionView = { let layout = makeCollectionLayout() let collection = UICollectionView(frame: .zero, collectionViewLayout: layout) collection.backgroundColor = AppDesignUIKit.pageBackground collection.delegate = self collection.register(HomeMoreMenuItemCell.self, forCellWithReuseIdentifier: HomeMoreMenuItemCell.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() title = "全部功能" view.backgroundColor = AppDesignUIKit.pageBackground navigationItem.largeTitleDisplayMode = .never configureDataSource() view.addSubview(collectionView) collectionView.snp.makeConstraints { make in make.edges.equalToSuperview() } viewModel.onChange = { [weak self] in self?.applySnapshot() } rebuildMenus() appServices.permissionContext.onChange = { [weak self] in self?.rebuildMenus() } } /// 构建 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.gridSection(itemHeight: 112) } let gridInsets = NSDirectionalEdgeInsets( top: AppMetrics.Spacing.xxSmall, leading: AppMetrics.Spacing.mediumLarge, bottom: AppMetrics.Spacing.xxSmall, trailing: AppMetrics.Spacing.mediumLarge ) let grid = CollectionDiffableLayout.gridSection( columns: 3, itemHeight: 112, interItemSpacing: 14, lineSpacing: AppMetrics.Spacing.mediumLarge, contentInsets: gridInsets ) switch section { case .commonApps: return CollectionDiffableLayout.addHeader(to: grid, height: 36) case .moreFunctions: return CollectionDiffableLayout.addHeader(to: grid, height: 36) } } } /// 注册 Diffable 数据源与 Cell 配置闭包。 private func configureDataSource() { dataSource = UICollectionViewDiffableDataSource( collectionView: collectionView ) { [weak self] collectionView, indexPath, item in guard let self, case .menu(let uri) = item, let section = self.dataSource.snapshot().sectionIdentifiers[safe: indexPath.section] else { return UICollectionViewCell() } let cell = collectionView.dequeueReusableCell( withReuseIdentifier: HomeMoreMenuItemCell.reuseID, for: indexPath ) as! HomeMoreMenuItemCell let isCommon = section == .commonApps let items = isCommon ? self.commonItems : self.moreItems guard let menuItem = items.first(where: { $0.uri == uri }) else { return cell } cell.configure(item: menuItem, isCommon: isCommon) { [weak self] in self?.toggleCommon(menuItem, isCommon: isCommon) } 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], let header = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: CollectionSectionHeaderView.reuseID, for: indexPath ) as? CollectionSectionHeaderView else { return nil } let title = section == .commonApps ? "常用应用" : "更多功能" header.configure(title: title) return header } } /// 根据当前常用应用与权限菜单构建 snapshot 并应用 diff 更新。 private func applySnapshot(animated: Bool = true) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.commonApps, .moreFunctions]) snapshot.appendItems( commonItems.map { HomeMoreItem.menu(uri: $0.uri) }, toSection: .commonApps ) snapshot.appendItems( moreItems.map { HomeMoreItem.menu(uri: $0.uri) }, toSection: .moreFunctions ) dataSource.apply(snapshot, animatingDifferences: animated) } /// 按当前角色权限重建首页菜单与常用应用。 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 var commonItems: [HomeMenuItem] { commonURIs.compactMap { menuItem(for: $0) } } private var moreItems: [HomeMenuItem] { viewModel.menuItems.filter { !isCommonURI($0.uri) } } /// 按 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 ) } /// 判断 URI 是否已在常用应用中。 private func isCommonURI(_ uri: String) -> Bool { let aliasKey = HomeMenuRouter.menuAliasKey(for: uri) return commonURIs.contains { HomeMenuRouter.menuAliasKey(for: $0) == aliasKey } } /// 切换菜单项的常用应用状态。 private func toggleCommon(_ item: HomeMenuItem, isCommon: Bool) { if isCommon { commonURIs = commonMenuStore.remove(item.uri, current: commonURIs) } else { commonURIs = commonMenuStore.add(item.uri, current: commonURIs, menuItems: viewModel.menuItems) } applySnapshot() } /// 打开菜单对应页面。 private func openMenu(_ item: HomeMenuItem) { let route = HomeMenuRouter.resolve(uri: item.uri, title: item.title) if case .destination(let homeRoute) = route, homeRoute == .moreFunctions { return } HomeMenuRouting.openRoute(route, from: self) } } // MARK: - UICollectionViewDelegate extension HomeMoreFunctionsViewController: UICollectionViewDelegate { /// 点击网格项打开对应功能页(加减按钮由 Cell 内部处理)。 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath), case .menu(let uri) = item else { return } let allItems = commonItems + moreItems guard let menuItem = allItems.first(where: { $0.uri == uri }) else { return } openMenu(menuItem) } } // MARK: - Menu Item Cell /// 全部功能页菜单网格单项 Cell,支持常用应用加减操作。 private final class HomeMoreMenuItemCell: UICollectionViewCell { static let reuseID = "HomeMoreMenuItemCell" private let iconView = UIImageView() private let titleLabel = UILabel() private let toggleButton = UIButton(type: .system) private var onToggle: (() -> Void)? /// 初始化实例。 override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = .white contentView.layer.cornerRadius = 8 titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.callout) titleLabel.textColor = UIColor(hex: 0x252525) titleLabel.textAlignment = .center titleLabel.numberOfLines = 1 titleLabel.adjustsFontSizeToFitWidth = true toggleButton.addTarget(self, action: #selector(toggleTapped), for: .touchUpInside) contentView.addSubview(toggleButton) let stack = UIStackView(arrangedSubviews: [iconView, titleLabel]) stack.axis = .vertical stack.spacing = AppMetrics.Spacing.mediumLarge + 1 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(34) } toggleButton.snp.makeConstraints { make in make.top.trailing.equalToSuperview().inset(2) make.width.height.equalTo(28) } iconView.contentMode = .scaleAspectFit } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 配置菜单项展示内容与常用应用切换按钮。 func configure(item: HomeMenuItem, isCommon: Bool, onToggle: @escaping () -> Void) { self.onToggle = onToggle 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 let iconName = isCommon ? "minus.circle.fill" : "plus.circle.fill" toggleButton.setImage(UIImage(systemName: iconName), for: .normal) toggleButton.tintColor = isCommon ? UIColor(hex: 0xFF1111) : AppDesignUIKit.primary } /// 点击加减按钮切换常用应用状态。 @objc private func toggleTapped() { onToggle?() } } /// 安全下标,避免 section 越界。 private extension Array { subscript(safe index: Int) -> Element? { indices.contains(index) ? self[index] : nil } }