// // OperatingAreaViewController.swift // suixinkan_ios // import SnapKit import UIKit /// 运营区域页:汇总信息、围栏地图与区域列表。 final class OperatingAreaViewController: UIViewController { private let viewModel = OperatingAreaViewModel() private let mapView = OperatingAreaMapView() private let tableView = UITableView(frame: .zero, style: .insetGrouped) private let summaryLabel = UILabel() private lazy var blockReasonView = AppContentUnavailableView(title: "无法展示地图", systemImage: "mappin.slash") private var dataSource: UITableViewDiffableDataSource! private var services: AppServices { AppServices.shared } /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = "运营区域" view.backgroundColor = UIColor(hex: 0xF5F7FA) setupLayout() wireViewModel() Task { await reload(showLoading: true) } } private func setupLayout() { summaryLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline) summaryLabel.textColor = AppDesign.textSecondary summaryLabel.numberOfLines = 0 mapView.snp.makeConstraints { make in make.height.equalTo(320) } blockReasonView.isHidden = true tableView.register(TitleSubtitleTableViewCell.self, forCellReuseIdentifier: TitleSubtitleTableViewCell.reuseIdentifier) tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(onPullRefresh), for: .valueChanged) dataSource = UITableViewDiffableDataSource(tableView: tableView) { [weak self] tableView, indexPath, itemID in guard let self else { return UITableViewCell() } let cell = tableView.dequeueReusableCell( withIdentifier: TitleSubtitleTableViewCell.reuseIdentifier, for: indexPath ) as! TitleSubtitleTableViewCell guard let item = self.viewModel.items.first(where: { String($0.id) == itemID }) else { return cell } let ringCount = self.viewModel.fenceRings.filter { $0.itemId == item.id }.count cell.configure( title: item.name.isEmpty ? "未命名区域" : item.name, subtitle: [item.typeText, item.statusText].filter { !$0.isEmpty }.joined(separator: " · "), detail: "围栏 \(ringCount) 个" ) return cell } let headerStack = UIStackView(arrangedSubviews: [summaryLabel, blockReasonView, mapView]) headerStack.axis = .vertical headerStack.spacing = AppMetrics.Spacing.medium let headerContainer = UIView() headerContainer.addSubview(headerStack) headerStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal) } headerContainer.layoutIfNeeded() let height = headerStack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height headerContainer.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: height + 24) tableView.tableHeaderView = headerContainer view.addSubview(tableView) tableView.snp.makeConstraints { make in make.edges.equalToSuperview() } } private func wireViewModel() { viewModel.onChange = { [weak self] in self?.render() } } private func render() { summaryLabel.text = "\(viewModel.title) · \(scopeText()) · 区域 \(viewModel.summary.areaCount) · 围栏 \(viewModel.summary.fenceCount)" if let reason = viewModel.blockReason { blockReasonView.isHidden = false blockReasonView.update(title: reason.message, systemImage: "mappin.slash") mapView.isHidden = true } else { blockReasonView.isHidden = true mapView.isHidden = false mapView.rings = viewModel.fenceRings } applyTableSnapshot() } /// 通过 Diffable snapshot 刷新区域列表。 private func applyTableSnapshot(animated: Bool = true) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(viewModel.items.map { String($0.id) }, toSection: 0) dataSource.apply(snapshot, animatingDifferences: animated) } private func scopeText() -> String { switch viewModel.mode { case .storeAdmin: return services.accountContext.currentStore?.name ?? "当前店铺" case .scenicAdmin: return services.accountContext.currentScenic?.name ?? "当前景区" case nil: return services.accountContext.currentScenic?.name ?? services.accountContext.currentStore?.name ?? "未选择业务范围" } } @objc private func onPullRefresh() { Task { await reload(showLoading: false) tableView.refreshControl?.endRefreshing() } } private func reload(showLoading: Bool) async { if showLoading { services.globalLoading.show() } defer { if showLoading { services.globalLoading.hide() } } await viewModel.reload( api: services.operatingAreaAPI, accountContext: services.accountContext, permissionContext: services.permissionContext ) if let message = viewModel.errorMessage { services.toastCenter.show(message) } } } extension OperatingAreaViewModel: ViewModelBindable {}