Implement permission-driven home tab aligned with Android.
Load role-permission on first visit to drive menus, role-based layout, store card, and location reporting with account-scoped persistence and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,27 +6,359 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页 Tab 根页面。
|
||||
/// 首页 Tab 根页面,按身份与权限展示信息与入口。
|
||||
final class HomeViewController: BaseViewController {
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let viewModel = HomeViewModel()
|
||||
private let homeAPI = NetworkServices.shared.homeAPI
|
||||
|
||||
private let scenicHeaderView = HomeScenicHeaderView()
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
private let workStatusCardView = HomeWorkStatusCardView()
|
||||
private let locationReportCardView = HomeLocationReportCardView()
|
||||
private let quickActionsView = HomeQuickActionsView()
|
||||
private let storeCardView = HomeStoreCardView()
|
||||
private let commonAppsGridView = HomeCommonAppsGridView()
|
||||
|
||||
private var hasInitialized = false
|
||||
private var countdownTimer: Timer?
|
||||
private var activeDialog: HomeDialogKind?
|
||||
|
||||
override func setupNavigationBar() {
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
titleLabel.text = "首页"
|
||||
titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.textColor = AppColor.text333
|
||||
view.addSubview(titleLabel)
|
||||
view.backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 12
|
||||
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
view.addSubview(scenicHeaderView)
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
contentStack.addArrangedSubview(workStatusCardView)
|
||||
contentStack.addArrangedSubview(locationReportCardView)
|
||||
contentStack.addArrangedSubview(quickActionsView)
|
||||
contentStack.addArrangedSubview(storeCardView)
|
||||
contentStack.addArrangedSubview(commonAppsGridView)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
scenicHeaderView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
scrollView.snp.makeConstraints { make in
|
||||
make.top.equalTo(scenicHeaderView.snp.bottom)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
make.width.equalTo(scrollView.snp.width).offset(-32)
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
||||
workStatusCardView.onOnlineTap = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||||
workStatusCardView.onReminderTap = { [weak self] in self?.presentReminderPicker() }
|
||||
locationReportCardView.onReportTap = { [weak self] in
|
||||
Task { await self?.viewModel.manualReportLocation(api: self?.homeAPI ?? NetworkServices.shared.homeAPI) }
|
||||
}
|
||||
quickActionsView.onCollectPayment = { [weak self] in self?.showToast("功能开发中") }
|
||||
quickActionsView.onSubmitTask = { [weak self] in self?.showToast("功能开发中") }
|
||||
quickActionsView.onToggleOnline = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||||
commonAppsGridView.onMenuSelected = { [weak self] menu in
|
||||
guard let self else { return }
|
||||
HomeRouteHandler.open(uri: menu.uri, from: self, storeItem: self.viewModel.storeItem)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAccountDidSwitch),
|
||||
name: NotificationName.accountDidSwitch,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleScenicDidChange),
|
||||
name: NotificationName.scenicDidChange,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if !hasInitialized {
|
||||
hasInitialized = true
|
||||
Task { await initializeHome() }
|
||||
} else {
|
||||
Task { await viewModel.reloadIfNeeded(api: homeAPI) }
|
||||
}
|
||||
startCountdownTimer()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func initializeHome() async {
|
||||
showLoading()
|
||||
defer { hideLoading() }
|
||||
await viewModel.initialize(api: homeAPI)
|
||||
applyViewModel()
|
||||
await evaluateDialogsWithDelay()
|
||||
await viewModel.refreshLocationInfo()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func evaluateDialogsWithDelay() async {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
await viewModel.evaluateDialogs(api: homeAPI)
|
||||
applyViewModel()
|
||||
presentDialogsIfNeeded()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
|
||||
let showWorkBlock = !viewModel.isMinimalTopRole
|
||||
workStatusCardView.isHidden = !showWorkBlock
|
||||
locationReportCardView.isHidden = !showWorkBlock
|
||||
quickActionsView.isHidden = !showWorkBlock
|
||||
|
||||
if showWorkBlock {
|
||||
workStatusCardView.apply(
|
||||
isOnline: viewModel.isOnline,
|
||||
countdownText: viewModel.countdownDisplayText,
|
||||
reminderMinutes: viewModel.reminderMinutes
|
||||
)
|
||||
locationReportCardView.apply(
|
||||
address: viewModel.locationInfoText,
|
||||
isReporting: viewModel.isLocationReporting
|
||||
)
|
||||
quickActionsView.apply(isOnline: viewModel.isOnline)
|
||||
}
|
||||
|
||||
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
||||
storeCardView.isHidden = !showStore
|
||||
if showStore, let store = viewModel.storeItem {
|
||||
storeCardView.apply(store: store)
|
||||
}
|
||||
|
||||
commonAppsGridView.apply(menus: viewModel.commonMenus)
|
||||
presentDialogsIfNeeded()
|
||||
}
|
||||
|
||||
private func presentDialogsIfNeeded() {
|
||||
if viewModel.showPermissionDialog {
|
||||
presentDialog(.permission)
|
||||
return
|
||||
}
|
||||
if viewModel.showScenicDialog {
|
||||
presentDialog(.scenic)
|
||||
return
|
||||
}
|
||||
if viewModel.showOnlineStatusDialog {
|
||||
presentDialog(.onlineStatus)
|
||||
return
|
||||
}
|
||||
if viewModel.showLocationTimeoutDialog {
|
||||
presentDialog(.locationTimeout)
|
||||
return
|
||||
}
|
||||
if viewModel.showLocationReportSuccessDialog {
|
||||
presentDialog(.locationSuccess)
|
||||
return
|
||||
}
|
||||
activeDialog = nil
|
||||
}
|
||||
|
||||
private enum HomeDialogKind {
|
||||
case permission
|
||||
case scenic
|
||||
case onlineStatus
|
||||
case locationTimeout
|
||||
case locationSuccess
|
||||
}
|
||||
|
||||
private func presentDialog(_ kind: HomeDialogKind) {
|
||||
guard activeDialog != kind, presentedViewController == nil else { return }
|
||||
activeDialog = kind
|
||||
switch kind {
|
||||
case .permission: presentPermissionDialog()
|
||||
case .scenic: presentScenicDialog()
|
||||
case .onlineStatus: presentOnlineStatusDialog()
|
||||
case .locationTimeout: presentLocationTimeoutDialog()
|
||||
case .locationSuccess: presentLocationSuccessDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private func presentPermissionDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "权限提示",
|
||||
message: "您还没有权限,请先申请权限",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hidePermissionDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "去申请", style: .default) { [weak self] _ in
|
||||
self?.showToast("功能开发中")
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentScenicDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "请选择景区",
|
||||
message: "使用首页功能前需要先选择当前景区",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideScenicDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "去选择", style: .default) { [weak self] _ in
|
||||
self?.viewModel.hideScenicDialog()
|
||||
self?.activeDialog = nil
|
||||
self?.presentScenicSelection()
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentOnlineStatusDialog() {
|
||||
let goingOnline = !viewModel.isOnline
|
||||
let alert = UIAlertController(
|
||||
title: goingOnline ? "切换在线" : "切换离线",
|
||||
message: goingOnline ? "确认切换到在线状态并上报位置?" : "确认切换到离线状态?",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideOnlineStatusSwitchDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentLocationTimeoutDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "位置上报提醒",
|
||||
message: "您的位置上报即将超时,请立即上报",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideLocationTimeoutDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "立即上报", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentLocationSuccessDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "上报成功",
|
||||
message: "上报时间:\(viewModel.reportTimeText)",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
self?.viewModel.hideLocationReportSuccessDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentReminderPicker() {
|
||||
let alert = UIAlertController(title: "提前提醒", message: "选择位置超时提前提醒时间", preferredStyle: .actionSheet)
|
||||
[0, 5, 10, 15, 30].forEach { minutes in
|
||||
let title = minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
|
||||
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||||
self?.viewModel.updateReminderMinutes(minutes)
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentScenicSelection() {
|
||||
let scenicList = AppStore.shared.roleScenicList()
|
||||
guard !scenicList.isEmpty else {
|
||||
showToast("暂无可选景区")
|
||||
return
|
||||
}
|
||||
let controller = ScenicSelectionViewController(scenicList: scenicList)
|
||||
controller.onSelected = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
await self.viewModel.reloadIfNeeded(api: self.homeAPI)
|
||||
await self.viewModel.loadStoreListIfNeeded(api: self.homeAPI)
|
||||
self.applyViewModel()
|
||||
await self.evaluateDialogsWithDelay()
|
||||
}
|
||||
}
|
||||
let nav = UINavigationController(rootViewController: controller)
|
||||
nav.modalPresentationStyle = .pageSheet
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
private func startCountdownTimer() {
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.viewModel.triggerLocationTimeoutIfNeeded()
|
||||
if !self.viewModel.isMinimalTopRole {
|
||||
self.workStatusCardView.apply(
|
||||
isOnline: self.viewModel.isOnline,
|
||||
countdownText: self.viewModel.countdownDisplayText,
|
||||
reminderMinutes: self.viewModel.reminderMinutes
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
viewModel.markNeedsPermissionReload()
|
||||
Task {
|
||||
await viewModel.reloadIfNeeded(api: homeAPI)
|
||||
applyViewModel()
|
||||
await evaluateDialogsWithDelay()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleScenicDidChange() {
|
||||
viewModel.refreshLocalDisplayState()
|
||||
Task {
|
||||
await viewModel.loadStoreListIfNeeded(api: homeAPI)
|
||||
applyViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
85
suixinkan/UI/Home/ScenicSelectionViewController.swift
Normal file
85
suixinkan/UI/Home/ScenicSelectionViewController.swift
Normal file
@ -0,0 +1,85 @@
|
||||
//
|
||||
// ScenicSelectionViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 景区选择页,使用 role-permission 返回的景区列表。
|
||||
final class ScenicSelectionViewController: BaseViewController {
|
||||
|
||||
var onSelected: (() -> Void)?
|
||||
|
||||
private let scenicList: [ScenicInfo]
|
||||
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
|
||||
init(scenicList: [ScenicInfo]) {
|
||||
self.scenicList = scenicList
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "选择景区"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .close,
|
||||
target: self,
|
||||
action: #selector(closeTapped)
|
||||
)
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = UIColor(hex: 0xF7FAFF)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
view.addSubview(tableView)
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closeTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScenicSelectionViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
scenicList.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
|
||||
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)
|
||||
}
|
||||
}
|
||||
132
suixinkan/UI/Home/Views/HomeCommonAppsGridView.swift
Normal file
132
suixinkan/UI/Home/Views/HomeCommonAppsGridView.swift
Normal file
@ -0,0 +1,132 @@
|
||||
//
|
||||
// HomeCommonAppsGridView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页常用应用网格。
|
||||
final class HomeCommonAppsGridView: UIView {
|
||||
|
||||
var onMenuSelected: ((HomeMenuItem) -> Void)?
|
||||
|
||||
private enum Section { case main }
|
||||
private enum Item: Hashable { case menu(HomeMenuItem) }
|
||||
|
||||
private var menus: [HomeMenuItem] = []
|
||||
private let titleLabel = UILabel()
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let itemSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(0.25),
|
||||
heightDimension: .absolute(88)
|
||||
)
|
||||
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
||||
let groupSize = NSCollectionLayoutSize(
|
||||
widthDimension: .fractionalWidth(1),
|
||||
heightDimension: .absolute(88)
|
||||
)
|
||||
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
||||
let section = NSCollectionLayoutSection(group: group)
|
||||
let layout = UICollectionViewCompositionalLayout(section: section)
|
||||
return UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
}()
|
||||
|
||||
private lazy var dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
|
||||
collectionView, indexPath, item in
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: HomeMenuCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! HomeMenuCell
|
||||
if case let .menu(menu) = item {
|
||||
cell.apply(menu: menu)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
titleLabel.text = "常用应用"
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
titleLabel.textColor = AppColor.text333
|
||||
|
||||
collectionView.backgroundColor = .clear
|
||||
collectionView.delegate = self
|
||||
collectionView.register(HomeMenuCell.self, forCellWithReuseIdentifier: HomeMenuCell.reuseIdentifier)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(collectionView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
}
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(12)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.equalTo(88)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(menus: [HomeMenuItem]) {
|
||||
self.menus = menus + [.moreFunctions]
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.menus.map { Item.menu($0) })
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeCommonAppsGridView: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
guard indexPath.item < menus.count else { return }
|
||||
onMenuSelected?(menus[indexPath.item])
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页菜单 cell。
|
||||
final class HomeMenuCell: UICollectionViewCell {
|
||||
|
||||
static let reuseIdentifier = "HomeMenuCell"
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
iconView.tintColor = AppColor.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: 12)
|
||||
titleLabel.textColor = AppColor.text333
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.numberOfLines = 2
|
||||
|
||||
contentView.addSubview(iconView)
|
||||
contentView.addSubview(titleLabel)
|
||||
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(8)
|
||||
make.centerX.equalToSuperview()
|
||||
make.width.height.equalTo(28)
|
||||
}
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(iconView.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(4)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(menu: HomeMenuItem) {
|
||||
iconView.image = UIImage(systemName: menu.iconName)
|
||||
titleLabel.text = menu.title
|
||||
}
|
||||
}
|
||||
70
suixinkan/UI/Home/Views/HomeLocationReportCardView.swift
Normal file
70
suixinkan/UI/Home/Views/HomeLocationReportCardView.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// HomeLocationReportCardView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页位置上报卡片。
|
||||
final class HomeLocationReportCardView: UIView {
|
||||
|
||||
var onReportTap: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let addressLabel = UILabel()
|
||||
private let reportButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
titleLabel.text = "位置上报"
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
titleLabel.textColor = AppColor.text333
|
||||
|
||||
addressLabel.font = .systemFont(ofSize: 13)
|
||||
addressLabel.textColor = AppColor.text666
|
||||
addressLabel.numberOfLines = 2
|
||||
|
||||
reportButton.setTitle("立即上报", for: .normal)
|
||||
reportButton.setTitleColor(.white, for: .normal)
|
||||
reportButton.backgroundColor = AppColor.primary
|
||||
reportButton.layer.cornerRadius = 6
|
||||
reportButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
reportButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 14, bottom: 8, right: 14)
|
||||
reportButton.addTarget(self, action: #selector(reportTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(addressLabel)
|
||||
addSubview(reportButton)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(15)
|
||||
}
|
||||
addressLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(8)
|
||||
make.leading.equalToSuperview().offset(15)
|
||||
make.bottom.equalToSuperview().offset(-15)
|
||||
make.trailing.lessThanOrEqualTo(reportButton.snp.leading).offset(-12)
|
||||
}
|
||||
reportButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-15)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(address: String, isReporting: Bool) {
|
||||
addressLabel.text = address
|
||||
reportButton.isEnabled = !isReporting
|
||||
reportButton.alpha = isReporting ? 0.6 : 1
|
||||
}
|
||||
|
||||
@objc private func reportTapped() { onReportTap?() }
|
||||
}
|
||||
86
suixinkan/UI/Home/Views/HomeQuickActionsView.swift
Normal file
86
suixinkan/UI/Home/Views/HomeQuickActionsView.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// HomeQuickActionsView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页快捷操作按钮区。
|
||||
final class HomeQuickActionsView: UIView {
|
||||
|
||||
var onCollectPayment: (() -> Void)?
|
||||
var onSubmitTask: (() -> Void)?
|
||||
var onToggleOnline: (() -> Void)?
|
||||
|
||||
private let stackView = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.spacing = 12
|
||||
addSubview(stackView)
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
make.height.equalTo(72)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(isOnline: Bool) {
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
stackView.addArrangedSubview(makeActionButton(title: "立即收款", icon: "yensign.circle", action: #selector(collectPaymentTapped)))
|
||||
stackView.addArrangedSubview(makeActionButton(title: "提交任务", icon: "doc.text", action: #selector(submitTaskTapped)))
|
||||
stackView.addArrangedSubview(makeActionButton(
|
||||
title: isOnline ? "在线" : "离线",
|
||||
icon: isOnline ? "dot.radiowaves.left.and.right" : "moon",
|
||||
action: #selector(toggleOnlineTapped)
|
||||
))
|
||||
}
|
||||
|
||||
private func makeActionButton(title: String, icon: String, action: Selector) -> UIView {
|
||||
let container = UIView()
|
||||
container.backgroundColor = .white
|
||||
container.layer.cornerRadius = 10
|
||||
|
||||
let button = UIButton(type: .system)
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
container.addSubview(button)
|
||||
button.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
let imageView = UIImageView(image: UIImage(systemName: icon))
|
||||
imageView.tintColor = AppColor.primary
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: 13, weight: .medium)
|
||||
label.textColor = AppColor.text333
|
||||
label.textAlignment = .center
|
||||
|
||||
let column = UIStackView(arrangedSubviews: [imageView, label])
|
||||
column.axis = .vertical
|
||||
column.alignment = .center
|
||||
column.spacing = 6
|
||||
column.isUserInteractionEnabled = false
|
||||
container.addSubview(column)
|
||||
column.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
imageView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(22)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
@objc private func collectPaymentTapped() { onCollectPayment?() }
|
||||
@objc private func submitTaskTapped() { onSubmitTask?() }
|
||||
@objc private func toggleOnlineTapped() { onToggleOnline?() }
|
||||
}
|
||||
56
suixinkan/UI/Home/Views/HomeScenicHeaderView.swift
Normal file
56
suixinkan/UI/Home/Views/HomeScenicHeaderView.swift
Normal file
@ -0,0 +1,56 @@
|
||||
//
|
||||
// HomeScenicHeaderView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页顶部景区选择条。
|
||||
final class HomeScenicHeaderView: UIView {
|
||||
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
titleLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
titleLabel.textColor = AppColor.text333
|
||||
arrowView.tintColor = AppColor.text666
|
||||
arrowView.contentMode = .scaleAspectFit
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(arrowView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.trailing.lessThanOrEqualTo(arrowView.snp.leading).offset(-8)
|
||||
}
|
||||
arrowView.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(16)
|
||||
}
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(scenicName: String) {
|
||||
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
||||
}
|
||||
|
||||
@objc private func handleTap() {
|
||||
onTap?()
|
||||
}
|
||||
}
|
||||
60
suixinkan/UI/Home/Views/HomeStoreCardView.swift
Normal file
60
suixinkan/UI/Home/Views/HomeStoreCardView.swift
Normal file
@ -0,0 +1,60 @@
|
||||
//
|
||||
// HomeStoreCardView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 店铺管理员门店信息卡片。
|
||||
final class HomeStoreCardView: UIView {
|
||||
|
||||
private let nameLabel = UILabel()
|
||||
private let addressLabel = UILabel()
|
||||
private let statusLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
nameLabel.textColor = AppColor.text333
|
||||
|
||||
addressLabel.font = .systemFont(ofSize: 13)
|
||||
addressLabel.textColor = AppColor.text666
|
||||
addressLabel.numberOfLines = 2
|
||||
|
||||
statusLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
statusLabel.textColor = AppColor.primary
|
||||
|
||||
addSubview(nameLabel)
|
||||
addSubview(addressLabel)
|
||||
addSubview(statusLabel)
|
||||
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(15)
|
||||
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-8)
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview().offset(15)
|
||||
make.trailing.equalToSuperview().offset(-15)
|
||||
}
|
||||
addressLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(nameLabel.snp.bottom).offset(8)
|
||||
make.leading.trailing.equalToSuperview().inset(15)
|
||||
make.bottom.equalToSuperview().offset(-15)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(store: StoreItem) {
|
||||
nameLabel.text = store.name
|
||||
addressLabel.text = store.address
|
||||
statusLabel.text = store.statusText.isEmpty ? "营业中" : store.statusText
|
||||
}
|
||||
}
|
||||
77
suixinkan/UI/Home/Views/HomeWorkStatusCardView.swift
Normal file
77
suixinkan/UI/Home/Views/HomeWorkStatusCardView.swift
Normal file
@ -0,0 +1,77 @@
|
||||
//
|
||||
// HomeWorkStatusCardView.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页在线状态与倒计时卡片。
|
||||
final class HomeWorkStatusCardView: UIView {
|
||||
|
||||
var onOnlineTap: (() -> Void)?
|
||||
var onReminderTap: (() -> Void)?
|
||||
|
||||
private let onlineButton = UIButton(type: .system)
|
||||
private let countdownLabel = UILabel()
|
||||
private let reminderButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
onlineButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
onlineButton.layer.cornerRadius = 4
|
||||
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
|
||||
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
|
||||
|
||||
countdownLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
countdownLabel.textColor = AppColor.primary
|
||||
|
||||
reminderButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
reminderButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
reminderButton.addTarget(self, action: #selector(reminderTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(onlineButton)
|
||||
addSubview(countdownLabel)
|
||||
addSubview(reminderButton)
|
||||
|
||||
onlineButton.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(15)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
countdownLabel.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
reminderButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-15)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(52)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(isOnline: Bool, countdownText: String, reminderMinutes: Int) {
|
||||
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
|
||||
if isOnline {
|
||||
onlineButton.backgroundColor = UIColor(hex: 0x22C55E)
|
||||
onlineButton.setTitleColor(UIColor(hex: 0xF0FDF4), for: .normal)
|
||||
} else {
|
||||
onlineButton.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
onlineButton.setTitleColor(UIColor(hex: 0x7B8EAA), for: .normal)
|
||||
}
|
||||
countdownLabel.text = countdownText
|
||||
let reminderTitle = reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
|
||||
reminderButton.setTitle(reminderTitle, for: .normal)
|
||||
}
|
||||
|
||||
@objc private func onlineTapped() { onOnlineTap?() }
|
||||
@objc private func reminderTapped() { onReminderTap?() }
|
||||
}
|
||||
Reference in New Issue
Block a user