// // PunchPointViewControllers.swift // suixinkan // // Created by Codex on 2026/6/26. // import CoreLocation import SnapKit import UIKit private extension PunchPointItem { var displayAddress: String { region?.address ?? scenicSpotStr } } /// 打卡点列表页。 final class PunchPointListViewController: ModuleTableViewController { private let viewModel = PunchPointListViewModel() /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { title = "打卡点" navigationItem.rightBarButtonItem = UIBarButtonItem( title: "新建", style: .plain, target: self, action: #selector(createPunchPoint) ) super.viewDidLoad() wireViewModel(viewModel) { } } /// table行Count相关逻辑。 override func tableRowCount() -> Int { viewModel.items.count } /// 配置Cell展示内容。 override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) { let item = viewModel.items[indexPath.row] cell.configure(title: item.name, subtitle: item.displayAddress, detail: item.statusLabel) } /// didSelectTableRow 回调处理。 override func didSelectTableRow(at indexPath: IndexPath) { let item = viewModel.items[indexPath.row] navigationController?.pushViewController( PunchPointDetailViewController(punchPointId: item.id, summary: item), animated: true ) } /// 刷新Content。 override func reloadContent() async { await viewModel.reload(scenicId: services.currentScenicId, api: services.punchPointAPI) } /// 创建打卡Point。 @objc private func createPunchPoint() { navigationController?.pushViewController(PunchPointEditorViewController(punchPointId: nil), animated: true) } } extension PunchPointListViewModel: ViewModelBindable {} /// 打卡点详情页。 final class PunchPointDetailViewController: ModuleTableViewController { private let punchPointId: Int private let summary: PunchPointItem? private let viewModel = PunchPointListViewModel() /// 初始化实例。 init(punchPointId: Int, summary: PunchPointItem?) { self.punchPointId = punchPointId self.summary = summary super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { title = summary?.name ?? "打卡点详情" navigationItem.rightBarButtonItem = UIBarButtonItem( title: "二维码", style: .plain, target: self, action: #selector(showQR) ) super.viewDidLoad() viewModel.onChange = { [weak self] in self?.title = self?.viewModel.selectedDetail?.name ?? self?.summary?.name self?.reloadTable() } } /// table行Count相关逻辑。 override func tableRowCount() -> Int { guard viewModel.selectedDetail != nil || summary != nil else { return 0 } return 4 } /// 配置Cell展示内容。 override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) { let detail = viewModel.selectedDetail ?? summary guard let detail else { return } switch indexPath.row { case 0: cell.configure(title: "名称", subtitle: detail.name) case 1: cell.configure(title: "地址", subtitle: detail.displayAddress) case 2: cell.configure(title: "状态", subtitle: detail.statusLabel) default: cell.configure(title: "创建时间", subtitle: detail.createdAt) } } /// 刷新Content。 override func reloadContent() async { await viewModel.loadDetail(id: punchPointId, api: services.punchPointAPI) } /// 展示QR。 @objc private func showQR() { guard let detail = viewModel.selectedDetail ?? summary else { return } navigationController?.pushViewController( PunchPointQRViewController(title: detail.name, qrURL: detail.mpQrcode), animated: true ) } } /// 打卡点编辑页。 final class PunchPointEditorViewController: UIViewController { private let viewModel = PunchPointEditorViewModel() private let punchPointId: Int? private let nameField = UITextField() private let addressField = UITextField() private let latitudeField = UITextField() private let longitudeField = UITextField() private let mapPicker = PunchPointMapPickerView() private let locationProvider = ForegroundLocationProvider() private var services: AppServices { AppServices.shared } init(punchPointId: Int?) { self.punchPointId = punchPointId super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() title = punchPointId == nil ? "新建打卡点" : "编辑打卡点" view.backgroundColor = UIColor(hex: 0xF5F7FA) navigationItem.rightBarButtonItem = UIBarButtonItem( title: "保存", style: .done, target: self, action: #selector(save) ) configureFields() layoutForm() wireViewModel() mapPicker.onLocationPicked = { [weak self] lat, lng, address in self?.viewModel.applyLocation(latitude: lat, longitude: lng, address: address) self?.syncFieldsFromViewModel() } Task { guard let punchPointId else { return } if let detail = try? await services.punchPointAPI.punchPointInfo(id: punchPointId) { viewModel.apply(detail) syncFieldsFromViewModel() } } } private func configureFields() { [nameField, addressField, latitudeField, longitudeField].forEach { $0.borderStyle = .roundedRect $0.font = .systemFont(ofSize: AppMetrics.FontSize.body) } nameField.placeholder = "打卡点名称" addressField.placeholder = "地址" latitudeField.placeholder = "纬度" longitudeField.placeholder = "经度" latitudeField.keyboardType = .decimalPad longitudeField.keyboardType = .decimalPad } private func layoutForm() { let locateButton = UIButton(type: .system) locateButton.setTitle("使用当前位置", for: .normal) locateButton.addTarget(self, action: #selector(useCurrentLocation), for: .touchUpInside) let stack = UIStackView(arrangedSubviews: [ nameField, mapPicker, addressField, latitudeField, longitudeField, locateButton ]) stack.axis = .vertical stack.spacing = AppMetrics.Spacing.small view.addSubview(stack) nameField.snp.makeConstraints { make in make.height.equalTo(44) } mapPicker.snp.makeConstraints { make in make.height.equalTo(220) } addressField.snp.makeConstraints { make in make.height.equalTo(44) } latitudeField.snp.makeConstraints { make in make.height.equalTo(44) } longitudeField.snp.makeConstraints { make in make.height.equalTo(44) } locateButton.snp.makeConstraints { make in make.height.equalTo(44) } stack.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide).offset(AppMetrics.Spacing.medium) make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal) } } private func wireViewModel() { viewModel.onChange = { [weak self] in self?.syncFieldsFromViewModel() } } private func syncFieldsFromViewModel() { if nameField.text?.isEmpty != false { nameField.text = viewModel.name } addressField.text = viewModel.address latitudeField.text = viewModel.latitudeText longitudeField.text = viewModel.longitudeText if let lat = Double(viewModel.latitudeText), let lng = Double(viewModel.longitudeText) { mapPicker.setCoordinate(latitude: lat, longitude: lng) } } @objc private func useCurrentLocation() { Task { do { let result = try await locationProvider.requestCurrentLocation() viewModel.applyLocation( latitude: result.latitude, longitude: result.longitude, address: result.address ) syncFieldsFromViewModel() } catch { services.toastCenter.show("定位失败:\(error.localizedDescription)") } } } @objc private func save() { viewModel.name = nameField.text ?? "" viewModel.address = addressField.text ?? "" viewModel.latitudeText = latitudeField.text ?? "" viewModel.longitudeText = longitudeField.text ?? "" Task { let success = await viewModel.submit( scenicId: services.currentScenicId, api: services.punchPointAPI, uploadService: services.ossUploadService ) if success { services.toastCenter.show("保存成功") navigationController?.popViewController(animated: true) } else if let message = viewModel.errorMessage { services.toastCenter.show(message) } } } } extension PunchPointEditorViewModel: ViewModelBindable {} /// 打卡点二维码页。 final class PunchPointQRViewController: UIViewController { private let pageTitle: String private let qrURL: String /// 初始化实例。 init(title: String, qrURL: String) { pageTitle = title self.qrURL = qrURL super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 视图加载完成后的 UI 初始化与数据绑定。 override func viewDidLoad() { super.viewDidLoad() title = pageTitle view.backgroundColor = UIColor(hex: 0xF5F7FA) let urlLabel = UILabel() urlLabel.text = qrURL urlLabel.numberOfLines = 0 urlLabel.textAlignment = .center urlLabel.textColor = AppDesign.textSecondary view.addSubview(urlLabel) urlLabel.snp.makeConstraints { make in make.center.equalToSuperview() make.leading.trailing.equalToSuperview().inset(24) } } }