添加景区选择与合作订单流程并对齐 Android
This commit is contained in:
@ -89,6 +89,11 @@ final class HomeViewController: BaseViewController {
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if !hasInitialized {
|
||||
@ -361,18 +366,17 @@ final class HomeViewController: BaseViewController {
|
||||
|
||||
private func presentScenicDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "请选择景区",
|
||||
message: "使用首页功能前需要先选择当前景区",
|
||||
title: "提示",
|
||||
message: "您还没有选定入驻景区,请先入驻",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideScenicDialog()
|
||||
self?.activeDialog = nil
|
||||
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?.viewModel.hideScenicDialog()
|
||||
alert.addAction(UIAlertAction(title: "去入驻", style: .default) { [weak self] _ in
|
||||
self?.activeDialog = nil
|
||||
self?.presentScenicSelection()
|
||||
self?.pushScenicSelection()
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
@ -440,24 +444,24 @@ final class HomeViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func presentScenicSelection() {
|
||||
let scenicList = AppStore.shared.roleScenicList()
|
||||
guard !scenicList.isEmpty else {
|
||||
showToast("暂无可选景区")
|
||||
return
|
||||
}
|
||||
let controller = ScenicSelectionViewController(scenicList: scenicList)
|
||||
controller.onSelected = { [weak self] in
|
||||
pushScenicSelection()
|
||||
}
|
||||
|
||||
private func pushScenicSelection() {
|
||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
let controller = ScenicSelectionViewController()
|
||||
controller.onSelectionComplete = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
await self.viewModel.reloadIfNeeded(api: self.homeAPI)
|
||||
self.viewModel.refreshLocalDisplayState()
|
||||
self.viewModel.rebuildCommonMenus()
|
||||
await self.viewModel.loadStoreListIfNeeded(api: self.homeAPI)
|
||||
self.applyViewModel()
|
||||
await self.evaluateDialogsWithDelay()
|
||||
await self.viewModel.evaluateDialogs(api: self.homeAPI)
|
||||
await MainActor.run { self.presentDialogsIfNeeded() }
|
||||
}
|
||||
}
|
||||
let nav = UINavigationController(rootViewController: controller)
|
||||
nav.modalPresentationStyle = .pageSheet
|
||||
present(nav, animated: true)
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
private func startCountdownTimer() {
|
||||
|
||||
194
suixinkan/UI/Home/PermissionApplyStatusViewController.swift
Normal file
194
suixinkan/UI/Home/PermissionApplyStatusViewController.swift
Normal file
@ -0,0 +1,194 @@
|
||||
//
|
||||
// PermissionApplyStatusViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 权限申请审核状态页,对齐 Android `PermissionApplyStatusScreen`。
|
||||
final class PermissionApplyStatusViewController: BaseViewController {
|
||||
|
||||
private let viewModel: PermissionApplyStatusViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let bannerView = PermissionApplyBannerView()
|
||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
init(
|
||||
applyCode: String,
|
||||
viewModel: PermissionApplyStatusViewModel? = nil,
|
||||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||||
) {
|
||||
self.viewModel = viewModel ?? PermissionApplyStatusViewModel(applyCode: applyCode)
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "权限申请"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 12
|
||||
scrollView.addSubview(contentStack)
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(loadingIndicator)
|
||||
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16))
|
||||
make.width.equalTo(scrollView).offset(-32)
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
loadingIndicator.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onNavigateToEdit = { [weak self] pending in
|
||||
Task { @MainActor in self?.navigateToEdit(pending: pending) }
|
||||
}
|
||||
bannerView.onApplyNewScenic = { [weak self] in
|
||||
self?.showToast("功能开发中")
|
||||
}
|
||||
Task { await viewModel.loadPendingData(api: homeAPI) }
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
if viewModel.isLoading {
|
||||
loadingIndicator.startAnimating()
|
||||
scrollView.isHidden = true
|
||||
return
|
||||
}
|
||||
loadingIndicator.stopAnimating()
|
||||
scrollView.isHidden = false
|
||||
contentStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
contentStack.addArrangedSubview(bannerView)
|
||||
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
if let pending = viewModel.pendingData, pending.status == PermissionApplyPendingStatus.rejected {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "去编辑",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(editTapped)
|
||||
)
|
||||
navigationItem.rightBarButtonItem?.tintColor = AppColor.primary
|
||||
}
|
||||
|
||||
guard let pending = viewModel.pendingData else { return }
|
||||
|
||||
contentStack.addArrangedSubview(makeStatusCard(pending: pending))
|
||||
contentStack.addArrangedSubview(makeSectionCard(title: "新增角色", chips: [pending.roleName]))
|
||||
if !pending.scenicList.isEmpty {
|
||||
contentStack.addArrangedSubview(
|
||||
makeSectionCard(title: "新增景区", chips: pending.scenicList.map(\.name))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeStatusCard(pending: RoleApplyPendingResponse) -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = AppColor.warningBackground
|
||||
card.layer.cornerRadius = 12
|
||||
card.layer.borderWidth = 1
|
||||
card.layer.borderColor = AppColor.warning.cgColor
|
||||
|
||||
let statusLabel = UILabel()
|
||||
statusLabel.text = pending.statusLabel
|
||||
statusLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
statusLabel.textColor = AppColor.warning
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
stack.addArrangedSubview(statusLabel)
|
||||
[
|
||||
("提交时间:", pending.createdAt),
|
||||
("审核时间:", pending.auditedAt ?? "--"),
|
||||
("审核人:", pending.auditedBy ?? "--"),
|
||||
("审核备注:", pending.auditNote.isEmpty ? "--" : pending.auditNote),
|
||||
].forEach { label, value in
|
||||
stack.addArrangedSubview(makeInfoRow(label: label, value: value))
|
||||
}
|
||||
|
||||
card.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeSectionCard(title: String, chips: [String]) -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 12
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
let chipStack = UIStackView()
|
||||
chipStack.axis = .horizontal
|
||||
chipStack.spacing = 8
|
||||
chips.forEach { name in
|
||||
chipStack.addArrangedSubview(PermissionTagChip(title: name, showsRemove: false))
|
||||
}
|
||||
|
||||
card.addSubview(titleLabel)
|
||||
card.addSubview(chipStack)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
chipStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(16)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeInfoRow(label: String, value: String) -> UIView {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .equalSpacing
|
||||
let left = UILabel()
|
||||
left.text = label
|
||||
left.font = .systemFont(ofSize: 14)
|
||||
left.textColor = AppColor.textSecondary
|
||||
let right = UILabel()
|
||||
right.text = value.isEmpty ? "--" : value
|
||||
right.font = .systemFont(ofSize: 14)
|
||||
right.textColor = AppColor.textSecondary
|
||||
right.textAlignment = .right
|
||||
row.addArrangedSubview(left)
|
||||
row.addArrangedSubview(right)
|
||||
return row
|
||||
}
|
||||
|
||||
@objc private func editTapped() {
|
||||
viewModel.navigateToEditIfRejected()
|
||||
}
|
||||
|
||||
private func navigateToEdit(pending: RoleApplyPendingResponse) {
|
||||
var controllers = navigationController?.viewControllers ?? []
|
||||
controllers.removeAll { $0 is PermissionApplyStatusViewController }
|
||||
let applyVC = PermissionApplyViewController(pendingData: pending)
|
||||
controllers.append(applyVC)
|
||||
navigationController?.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
}
|
||||
208
suixinkan/UI/Home/PermissionApplyViewController.swift
Normal file
208
suixinkan/UI/Home/PermissionApplyViewController.swift
Normal file
@ -0,0 +1,208 @@
|
||||
//
|
||||
// PermissionApplyViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 权限申请页,对齐 Android `PermissionApplyScreen`。
|
||||
final class PermissionApplyViewController: BaseViewController {
|
||||
|
||||
private let viewModel: PermissionApplyViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
private let pendingData: RoleApplyPendingResponse?
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let bannerView = PermissionApplyBannerView()
|
||||
private let roleSelector = PermissionFormSelectorRow(title: "角色信息", subtitle: "单选 · 请选择您需要申请权限的角色", placeholder: "请选择角色")
|
||||
private let existingScenicTags = PermissionSelectedTagsView()
|
||||
private let scenicSelector = PermissionFormSelectorRow(title: "新增景区", subtitle: "多选 · 请选择需要新增权限的景区", placeholder: "请选择景区")
|
||||
private let selectedScenicTags = PermissionSelectedTagsView()
|
||||
private let submitButton = PermissionSubmitButton()
|
||||
|
||||
init(
|
||||
pendingData: RoleApplyPendingResponse? = nil,
|
||||
viewModel: PermissionApplyViewModel = PermissionApplyViewModel(),
|
||||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||||
) {
|
||||
self.pendingData = pendingData
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "申请权限"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 8
|
||||
let cardStack = UIStackView(arrangedSubviews: [roleSelector, existingScenicTags, scenicSelector, selectedScenicTags])
|
||||
cardStack.axis = .vertical
|
||||
cardStack.spacing = 20
|
||||
card.addSubview(cardStack)
|
||||
cardStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
}
|
||||
|
||||
contentStack.addArrangedSubview(bannerView)
|
||||
contentStack.addArrangedSubview(card)
|
||||
scrollView.addSubview(contentStack)
|
||||
view.addSubview(scrollView)
|
||||
view.addSubview(submitButton)
|
||||
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16))
|
||||
make.width.equalTo(scrollView).offset(-32)
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.bottom.equalTo(submitButton.snp.top).offset(-12)
|
||||
}
|
||||
submitButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
viewModel.onSubmitSuccess = { [weak self] applyCode in
|
||||
Task { @MainActor in self?.handleSubmitSuccess(applyCode: applyCode) }
|
||||
}
|
||||
|
||||
bannerView.onApplyNewScenic = { [weak self] in
|
||||
self?.showToast("功能开发中")
|
||||
}
|
||||
roleSelector.onTap = { [weak self] in self?.presentRolePicker() }
|
||||
scenicSelector.onTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.viewModel.showScenicPickerSheet()
|
||||
if self.viewModel.availableScenics.isEmpty {
|
||||
Task { await self.viewModel.loadAvailableScenics(api: self.homeAPI) }
|
||||
}
|
||||
self.presentScenicPickerIfNeeded()
|
||||
}
|
||||
submitButton.addTarget(self, action: #selector(submitTapped), for: .touchUpInside)
|
||||
|
||||
if let pendingData {
|
||||
viewModel.initWithPendingData(pendingData)
|
||||
Task { await viewModel.loadAvailableScenics(api: homeAPI) }
|
||||
}
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
if viewModel.selectedRoleName.isEmpty {
|
||||
roleSelector.setValue("请选择角色", isPlaceholder: true)
|
||||
existingScenicTags.isHidden = true
|
||||
} else {
|
||||
roleSelector.setValue(viewModel.selectedRoleName, isPlaceholder: false)
|
||||
if let roleId = viewModel.selectedRoleId {
|
||||
let names = viewModel.existingScenics(for: roleId).map(\.name)
|
||||
existingScenicTags.apply(title: "已开通景区", names: names, onRemove: nil)
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.selectedScenicNames.isEmpty {
|
||||
scenicSelector.setValue("请选择景区", isPlaceholder: true)
|
||||
selectedScenicTags.isHidden = true
|
||||
} else {
|
||||
scenicSelector.setValue("已选 \(viewModel.selectedScenicNames.count) 个景区", isPlaceholder: false)
|
||||
selectedScenicTags.apply(
|
||||
title: "待申请景区",
|
||||
names: viewModel.selectedScenicNames
|
||||
) { [weak self] index in
|
||||
guard let self else { return }
|
||||
let id = self.viewModel.selectedScenicIds[index]
|
||||
let name = self.viewModel.selectedScenicNames[index]
|
||||
self.viewModel.removeSelectedScenic(id: id, name: name)
|
||||
}
|
||||
}
|
||||
|
||||
submitButton.setSubmitting(viewModel.isSubmitting)
|
||||
presentScenicPickerIfNeeded()
|
||||
presentRolePickerIfNeeded()
|
||||
}
|
||||
|
||||
private func presentRolePickerIfNeeded() {
|
||||
guard viewModel.showRolePicker else { return }
|
||||
viewModel.hideRolePickerSheet()
|
||||
presentRolePicker()
|
||||
}
|
||||
|
||||
private func presentRolePicker() {
|
||||
let alert = UIAlertController(title: "选择角色", message: nil, preferredStyle: .actionSheet)
|
||||
viewModel.availableRoles.forEach { role in
|
||||
alert.addAction(UIAlertAction(title: role.name, style: .default) { [weak self] _ in
|
||||
self?.viewModel.selectRole(role)
|
||||
Task { await self?.viewModel.loadAvailableScenics(api: self?.homeAPI) }
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentScenicPickerIfNeeded() {
|
||||
guard viewModel.showScenicPicker else { return }
|
||||
viewModel.hideScenicPickerSheet()
|
||||
presentScenicPicker()
|
||||
}
|
||||
|
||||
private func presentScenicPicker() {
|
||||
guard viewModel.selectedRoleId != nil else {
|
||||
showToast("请先选择角色")
|
||||
return
|
||||
}
|
||||
if viewModel.availableScenics.isEmpty {
|
||||
Task {
|
||||
await viewModel.loadAvailableScenics(api: homeAPI)
|
||||
await MainActor.run { self.presentScenicPicker() }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let alert = UIAlertController(title: "选择景区", message: nil, preferredStyle: .actionSheet)
|
||||
viewModel.availableScenics.filter { !$0.isDisabled }.forEach { scenic in
|
||||
let prefix = scenic.isSelected ? "✓ " : ""
|
||||
alert.addAction(UIAlertAction(title: "\(prefix)\(scenic.name)", style: .default) { [weak self] _ in
|
||||
self?.viewModel.toggleScenic(scenic)
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "完成", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func submitTapped() {
|
||||
Task { await viewModel.submit(api: homeAPI) }
|
||||
}
|
||||
|
||||
private func handleSubmitSuccess(applyCode: String) {
|
||||
guard !applyCode.isEmpty else {
|
||||
navigationController?.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
var controllers = navigationController?.viewControllers ?? []
|
||||
controllers.removeAll { $0 === self }
|
||||
let statusVC = PermissionApplyStatusViewController(applyCode: applyCode)
|
||||
controllers.append(statusVC)
|
||||
navigationController?.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
}
|
||||
@ -6,16 +6,23 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 景区选择页,使用 role-permission 返回的景区列表。
|
||||
final class ScenicSelectionViewController: BaseViewController {
|
||||
/// 景区选择页,对齐 Android `ScenicSelectionScreen`。
|
||||
final class ScenicSelectionViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
|
||||
var onSelected: (() -> Void)?
|
||||
var onSelectionComplete: (() -> Void)?
|
||||
|
||||
private let scenicList: [ScenicInfo]
|
||||
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
private let viewModel: ScenicSelectionViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
init(scenicList: [ScenicInfo]) {
|
||||
self.scenicList = scenicList
|
||||
private let bannerView = ScenicPermissionBannerView()
|
||||
private let searchBar = ScenicSelectionSearchBar()
|
||||
private let locationBar = ScenicCurrentLocationBar()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let emptyLabel = UILabel()
|
||||
|
||||
init(viewModel: ScenicSelectionViewModel = ScenicSelectionViewModel(), homeAPI: HomeAPI = NetworkServices.shared.homeAPI) {
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@ -25,61 +32,122 @@ final class ScenicSelectionViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "选择景区"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .close,
|
||||
target: self,
|
||||
action: #selector(closeTapped)
|
||||
)
|
||||
title = "景区选择"
|
||||
navigationController?.setNavigationBarHidden(false, animated: false)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
view.backgroundColor = .white
|
||||
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
view.addSubview(tableView)
|
||||
}
|
||||
tableView.register(ScenicSpotCell.self, forCellReuseIdentifier: ScenicSpotCell.reuseIdentifier)
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
|
||||
override func setupConstraints() {
|
||||
emptyLabel.text = "暂无景区"
|
||||
emptyLabel.font = .systemFont(ofSize: 14)
|
||||
emptyLabel.textColor = AppColor.textTertiary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.isHidden = true
|
||||
|
||||
let headerStack = UIStackView(arrangedSubviews: [bannerView, searchBar, locationBar])
|
||||
headerStack.axis = .vertical
|
||||
headerStack.spacing = 16
|
||||
headerStack.isLayoutMarginsRelativeArrangement = true
|
||||
headerStack.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
||||
|
||||
view.addSubview(headerStack)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyLabel)
|
||||
|
||||
headerStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
make.top.equalTo(headerStack.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
emptyLabel.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
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) }
|
||||
}
|
||||
viewModel.onSelectionComplete = { [weak self] in
|
||||
Task { @MainActor in
|
||||
self?.onSelectionComplete?()
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
viewModel.onNavigateApply = { [weak self] in
|
||||
Task { @MainActor in self?.pushPermissionApply() }
|
||||
}
|
||||
viewModel.onNavigateApplyStatus = { [weak self] applyCode in
|
||||
Task { @MainActor in self?.pushPermissionApplyStatus(applyCode: applyCode) }
|
||||
}
|
||||
|
||||
bannerView.onApplyNow = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.onApplyNow(api: self.homeAPI) }
|
||||
}
|
||||
searchBar.onSearchTextChange = { [weak self] query in
|
||||
self?.viewModel.onSearchQueryChange(query)
|
||||
}
|
||||
locationBar.onRelocate = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.startLocation() }
|
||||
}
|
||||
|
||||
viewModel.loadFromLocal()
|
||||
Task { await viewModel.startLocation() }
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
locationBar.apply(locationText: viewModel.currentLocationText)
|
||||
emptyLabel.isHidden = !viewModel.spots.isEmpty
|
||||
tableView.reloadData()
|
||||
if viewModel.isLoading {
|
||||
showLoading()
|
||||
} else {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
private func pushPermissionApply(pendingData: RoleApplyPendingResponse? = nil) {
|
||||
let controller = PermissionApplyViewController(pendingData: pendingData)
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
private func pushPermissionApplyStatus(applyCode: String) {
|
||||
let controller = PermissionApplyStatusViewController(applyCode: applyCode)
|
||||
navigationController?.pushViewController(controller, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
extension ScenicSelectionViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
scenicList.count
|
||||
viewModel.spots.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = scenicList[indexPath.row].name
|
||||
cell.contentConfiguration = config
|
||||
cell.accessoryType = scenicList[indexPath.row].id == AppStore.shared.currentScenicId ? .checkmark : .none
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: ScenicSpotCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! ScenicSpotCell
|
||||
let spot = viewModel.spots[indexPath.row]
|
||||
cell.apply(spot)
|
||||
cell.onTap = { [weak self] in
|
||||
self?.viewModel.selectSpot(spot)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let scenic = scenicList[indexPath.row]
|
||||
AppStore.shared.currentScenicId = scenic.id
|
||||
AppStore.shared.currentScenicName = scenic.name
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.scenicDidChange,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
NotificationUserInfoKey.scenicId: scenic.id,
|
||||
NotificationUserInfoKey.scenicName: scenic.name,
|
||||
]
|
||||
)
|
||||
onSelected?()
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
252
suixinkan/UI/Home/Views/PermissionApplyViews.swift
Normal file
252
suixinkan/UI/Home/Views/PermissionApplyViews.swift
Normal file
@ -0,0 +1,252 @@
|
||||
//
|
||||
// PermissionApplyViews.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 权限申请页橙色横幅(新景区申请入口暂 toast)。
|
||||
final class PermissionApplyBannerView: UIView {
|
||||
|
||||
var onApplyNewScenic: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let applyButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = AppColor.warningBackground
|
||||
|
||||
titleLabel.text = "希望开通全新景区?"
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.warning
|
||||
|
||||
applyButton.setTitle("新景区申请 ›", for: .normal)
|
||||
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
applyButton.setTitleColor(AppColor.warning, for: .normal)
|
||||
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(applyButton)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
applyButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func applyTapped() {
|
||||
onApplyNewScenic?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 表单字段选择行。
|
||||
final class PermissionFormSelectorRow: UIView {
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let valueLabel = UILabel()
|
||||
private let chevron = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
|
||||
init(title: String, subtitle: String, placeholder: String) {
|
||||
super.init(frame: .zero)
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.font = .systemFont(ofSize: 12)
|
||||
subtitleLabel.textColor = AppColor.textTertiary
|
||||
|
||||
valueLabel.text = placeholder
|
||||
valueLabel.font = .systemFont(ofSize: 14)
|
||||
valueLabel.textColor = AppColor.textTertiary
|
||||
|
||||
chevron.tintColor = AppColor.textTertiary
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped))
|
||||
addGestureRecognizer(tap)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(subtitleLabel)
|
||||
addSubview(valueLabel)
|
||||
addSubview(chevron)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview()
|
||||
}
|
||||
subtitleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(4)
|
||||
make.leading.equalToSuperview()
|
||||
}
|
||||
valueLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(subtitleLabel.snp.bottom).offset(12)
|
||||
make.leading.bottom.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(chevron.snp.leading).offset(-8)
|
||||
}
|
||||
chevron.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalTo(valueLabel)
|
||||
make.size.equalTo(14)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setValue(_ text: String, isPlaceholder: Bool) {
|
||||
valueLabel.text = text
|
||||
valueLabel.textColor = isPlaceholder ? AppColor.textTertiary : AppColor.textPrimary
|
||||
}
|
||||
|
||||
@objc private func rowTapped() {
|
||||
onTap?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 已选景区标签容器。
|
||||
final class PermissionSelectedTagsView: UIView {
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let stackView = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = 8
|
||||
addSubview(titleLabel)
|
||||
addSubview(stackView)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
}
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(8)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(title: String, names: [String], onRemove: ((Int) -> Void)? = nil) {
|
||||
titleLabel.text = title
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
guard !names.isEmpty else {
|
||||
isHidden = true
|
||||
return
|
||||
}
|
||||
isHidden = false
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.spacing = 8
|
||||
row.alignment = .leading
|
||||
names.enumerated().forEach { index, name in
|
||||
let chip = PermissionTagChip(title: name)
|
||||
if let onRemove {
|
||||
chip.onRemove = { onRemove(index) }
|
||||
}
|
||||
row.addArrangedSubview(chip)
|
||||
}
|
||||
stackView.addArrangedSubview(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签 Chip。
|
||||
final class PermissionTagChip: UIView {
|
||||
|
||||
var onRemove: (() -> Void)?
|
||||
|
||||
private let label = UILabel()
|
||||
private let removeButton = UIButton(type: .system)
|
||||
|
||||
init(title: String, showsRemove: Bool = true) {
|
||||
super.init(frame: .zero)
|
||||
layer.cornerRadius = 4
|
||||
layer.borderWidth = 1
|
||||
layer.borderColor = AppColor.primary.cgColor
|
||||
backgroundColor = .white
|
||||
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = AppColor.primary
|
||||
|
||||
removeButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
removeButton.tintColor = AppColor.primary
|
||||
removeButton.addTarget(self, action: #selector(removeTapped), for: .touchUpInside)
|
||||
removeButton.isHidden = !showsRemove
|
||||
|
||||
addSubview(label)
|
||||
addSubview(removeButton)
|
||||
label.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.top.bottom.equalToSuperview().inset(8)
|
||||
if showsRemove {
|
||||
make.trailing.equalTo(removeButton.snp.leading).offset(-4)
|
||||
} else {
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
}
|
||||
}
|
||||
removeButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-8)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(20)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func removeTapped() {
|
||||
onRemove?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部主按钮。
|
||||
final class PermissionSubmitButton: UIButton {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setTitle("提交申请", for: .normal)
|
||||
setTitleColor(.white, for: .normal)
|
||||
titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
backgroundColor = AppColor.primary
|
||||
layer.cornerRadius = 8
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(48)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setSubmitting(_ submitting: Bool) {
|
||||
isEnabled = !submitting
|
||||
alpha = submitting ? 0.6 : 1
|
||||
setTitle(submitting ? "提交中..." : "提交申请", for: .normal)
|
||||
}
|
||||
}
|
||||
300
suixinkan/UI/Home/Views/ScenicSelectionViews.swift
Normal file
300
suixinkan/UI/Home/Views/ScenicSelectionViews.swift
Normal file
@ -0,0 +1,300 @@
|
||||
//
|
||||
// ScenicSelectionViews.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 景区选择页橙色权限申请横幅。
|
||||
final class ScenicPermissionBannerView: UIView {
|
||||
|
||||
var onApplyNow: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let applyButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = AppColor.warningBackground
|
||||
|
||||
titleLabel.text = "希望开通更多景区权限"
|
||||
titleLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
titleLabel.textColor = AppColor.warning
|
||||
titleLabel.numberOfLines = 2
|
||||
|
||||
applyButton.setTitle("立即申请 ›", for: .normal)
|
||||
applyButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
applyButton.setTitleColor(AppColor.warning, for: .normal)
|
||||
applyButton.addTarget(self, action: #selector(applyTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(applyButton)
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(applyButton.snp.leading).offset(-8)
|
||||
}
|
||||
applyButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.greaterThanOrEqualTo(44)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func applyTapped() {
|
||||
onApplyNow?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区搜索栏。
|
||||
final class ScenicSelectionSearchBar: UIView, UITextFieldDelegate {
|
||||
|
||||
var onSearchTextChange: ((String) -> Void)?
|
||||
|
||||
let textField = UITextField()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor(hex: 0xF2F4F8)
|
||||
layer.cornerRadius = 12
|
||||
clipsToBounds = true
|
||||
|
||||
let icon = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||
icon.tintColor = UIColor(hex: 0xB3B8C2)
|
||||
icon.contentMode = .scaleAspectFit
|
||||
|
||||
textField.placeholder = "搜索景区"
|
||||
textField.font = .systemFont(ofSize: 14)
|
||||
textField.textColor = AppColor.textPrimary
|
||||
textField.returnKeyType = .search
|
||||
textField.delegate = self
|
||||
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
|
||||
|
||||
addSubview(icon)
|
||||
addSubview(textField)
|
||||
icon.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(12)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(18)
|
||||
}
|
||||
textField.snp.makeConstraints { make in
|
||||
make.leading.equalTo(icon.snp.trailing).offset(8)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.top.bottom.equalToSuperview().inset(10)
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(44)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func textChanged() {
|
||||
onSearchTextChange?(textField.text ?? "")
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前定位栏。
|
||||
final class ScenicCurrentLocationBar: UIView {
|
||||
|
||||
var onRelocate: (() -> Void)?
|
||||
|
||||
private let iconView = UIImageView(image: UIImage(systemName: "location.fill"))
|
||||
private let locationLabel = UILabel()
|
||||
private let relocateButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
iconView.tintColor = AppColor.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
|
||||
locationLabel.font = .systemFont(ofSize: 14)
|
||||
locationLabel.textColor = AppColor.textPrimary
|
||||
locationLabel.numberOfLines = 2
|
||||
|
||||
relocateButton.setTitle("重新定位", for: .normal)
|
||||
relocateButton.titleLabel?.font = .systemFont(ofSize: 12)
|
||||
relocateButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
relocateButton.backgroundColor = AppColor.primaryLight
|
||||
relocateButton.layer.cornerRadius = 4
|
||||
relocateButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8)
|
||||
relocateButton.addTarget(self, action: #selector(relocateTapped), for: .touchUpInside)
|
||||
relocateButton.setContentHuggingPriority(.required, for: .horizontal)
|
||||
relocateButton.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
||||
addSubview(iconView)
|
||||
addSubview(locationLabel)
|
||||
addSubview(relocateButton)
|
||||
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(16)
|
||||
}
|
||||
locationLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(iconView.snp.trailing).offset(4)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(relocateButton.snp.leading).offset(-12)
|
||||
}
|
||||
relocateButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview()
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(locationText: String) {
|
||||
locationLabel.text = locationText
|
||||
}
|
||||
|
||||
@objc private func relocateTapped() {
|
||||
onRelocate?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区卡片 Cell。
|
||||
final class ScenicSpotCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "ScenicSpotCell"
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
private let cardView = UIView()
|
||||
private let coverImageView = UIImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let distanceLabel = UILabel()
|
||||
private let addressLabel = UILabel()
|
||||
private let statusTag = UILabel()
|
||||
private let selectedBorderColor = UIColor(hex: 0xE74C3C)
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
|
||||
cardView.backgroundColor = AppColor.pageBackgroundSoft
|
||||
cardView.layer.cornerRadius = 8
|
||||
cardView.layer.borderWidth = 2
|
||||
cardView.layer.borderColor = UIColor(hex: 0xF4F4F4).cgColor
|
||||
|
||||
coverImageView.contentMode = .scaleAspectFill
|
||||
coverImageView.clipsToBounds = true
|
||||
coverImageView.layer.cornerRadius = 6
|
||||
coverImageView.backgroundColor = UIColor(hex: 0xF0F0F0)
|
||||
|
||||
nameLabel.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
nameLabel.textColor = AppColor.textPrimary
|
||||
|
||||
distanceLabel.font = .systemFont(ofSize: 12)
|
||||
distanceLabel.textColor = AppColor.textSecondary
|
||||
|
||||
addressLabel.font = .systemFont(ofSize: 12)
|
||||
addressLabel.textColor = AppColor.textTertiary
|
||||
addressLabel.numberOfLines = 2
|
||||
|
||||
statusTag.font = .systemFont(ofSize: 11, weight: .medium)
|
||||
statusTag.textColor = .white
|
||||
statusTag.textAlignment = .center
|
||||
statusTag.layer.cornerRadius = 4
|
||||
statusTag.clipsToBounds = true
|
||||
statusTag.isHidden = true
|
||||
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(coverImageView)
|
||||
cardView.addSubview(nameLabel)
|
||||
cardView.addSubview(statusTag)
|
||||
cardView.addSubview(distanceLabel)
|
||||
cardView.addSubview(addressLabel)
|
||||
|
||||
cardView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
|
||||
}
|
||||
coverImageView.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(12)
|
||||
make.size.equalTo(80)
|
||||
}
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(coverImageView.snp.trailing).offset(12)
|
||||
make.top.equalToSuperview().offset(12)
|
||||
make.trailing.lessThanOrEqualTo(statusTag.snp.leading).offset(-8)
|
||||
}
|
||||
statusTag.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.centerY.equalTo(nameLabel)
|
||||
make.height.equalTo(20)
|
||||
}
|
||||
distanceLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(nameLabel)
|
||||
make.top.equalTo(nameLabel.snp.bottom).offset(6)
|
||||
}
|
||||
addressLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(nameLabel)
|
||||
make.trailing.equalToSuperview().offset(-12)
|
||||
make.top.equalTo(distanceLabel.snp.bottom).offset(4)
|
||||
make.bottom.lessThanOrEqualToSuperview().offset(-12)
|
||||
}
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
|
||||
cardView.addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(_ item: ScenicSpotDisplayItem) {
|
||||
nameLabel.text = item.name
|
||||
distanceLabel.text = "\(item.distanceText)km"
|
||||
addressLabel.text = item.address
|
||||
|
||||
if let url = URL(string: item.coverImg), !item.coverImg.isEmpty {
|
||||
coverImageView.kf.setImage(with: url, placeholder: UIImage(systemName: "photo"))
|
||||
} else {
|
||||
coverImageView.image = UIImage(systemName: "photo")
|
||||
coverImageView.tintColor = AppColor.textTertiary
|
||||
}
|
||||
|
||||
if let label = item.statusLabel {
|
||||
statusTag.isHidden = false
|
||||
statusTag.text = " \(label) "
|
||||
switch item.openStatus {
|
||||
case .nearest, .closed:
|
||||
statusTag.backgroundColor = selectedBorderColor
|
||||
case .none:
|
||||
statusTag.backgroundColor = AppColor.textTertiary
|
||||
}
|
||||
} else {
|
||||
statusTag.isHidden = true
|
||||
}
|
||||
|
||||
cardView.layer.borderColor = (item.isSelected ? selectedBorderColor : UIColor(hex: 0xF4F4F4)).cgColor
|
||||
}
|
||||
|
||||
@objc private func cardTapped() {
|
||||
onTap?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user