Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,59 @@
|
||||
//
|
||||
// HomeMenuRouting.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
/// 首页菜单 UIKit 路由,将权限 URI 解析结果映射为 Tab 切换或 Navigation push。
|
||||
enum HomeMenuRouting {
|
||||
|
||||
/// 打开首页菜单项对应的目标。
|
||||
static func openMenu(_ item: HomeMenuItem, from viewController: UIViewController) {
|
||||
openRoute(HomeMenuRouter.resolve(uri: item.uri, title: item.title), from: viewController)
|
||||
}
|
||||
|
||||
/// 打开首页 URI 解析结果。
|
||||
static func openRoute(_ route: HomeMenuResolvedRoute, from viewController: UIViewController) {
|
||||
let services = AppServices.shared
|
||||
switch route {
|
||||
case .tab(let tab):
|
||||
services.appRouter.select(tab)
|
||||
case .orders(let entry):
|
||||
services.appRouter.selectOrders(entry: entry)
|
||||
case .destination(let homeRoute):
|
||||
push(homeRoute, from: viewController)
|
||||
case .unsupported(let uri, let title, _):
|
||||
pushPlaceholder(title: title, uri: uri, from: viewController)
|
||||
case .placeholder(let uri, let title):
|
||||
HomeRouteDiagnostics.recordUnknown(uri: uri, title: title)
|
||||
pushPlaceholder(title: title, uri: uri, from: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
/// Push 首页二级路由页面。
|
||||
static func push(_ route: HomeRoute, from viewController: UIViewController) {
|
||||
let target = AppRouteViewControllerFactory.makeViewController(for: route, services: AppServices.shared)
|
||||
viewController.navigationController?.pushViewController(target, animated: true)
|
||||
}
|
||||
|
||||
/// Push 订单模块路由页面。
|
||||
static func pushOrders(_ route: OrdersRoute, from viewController: UIViewController) {
|
||||
let target = AppRouteViewControllerFactory.makeViewController(for: .orders(route), services: AppServices.shared)
|
||||
viewController.navigationController?.pushViewController(target, animated: true)
|
||||
}
|
||||
|
||||
/// Push 个人中心二级路由页面。
|
||||
static func pushProfile(_ route: ProfileRoute, from viewController: UIViewController) {
|
||||
let target = AppRouteViewControllerFactory.makeViewController(for: .profile(route), services: AppServices.shared)
|
||||
viewController.navigationController?.pushViewController(target, animated: true)
|
||||
}
|
||||
|
||||
private static func pushPlaceholder(title: String, uri: String, from viewController: UIViewController) {
|
||||
viewController.navigationController?.pushViewController(
|
||||
FeaturePlaceholderViewController(title: title, uri: uri),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,260 @@
|
||||
//
|
||||
// HomeMoreFunctionsViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页全部功能页,支持常用应用增删和更多功能网格展示。
|
||||
final class HomeMoreFunctionsViewController: UIViewController {
|
||||
|
||||
private let viewModel = HomeViewModel()
|
||||
private let commonMenuStore = HomeCommonMenuStore()
|
||||
private var commonURIs: [String] = []
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.separatorStyle = .none
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(HomeMoreMenuGridCell.self, forCellReuseIdentifier: HomeMoreMenuGridCell.reuseID)
|
||||
return table
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = "全部功能"
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
navigationItem.largeTitleDisplayMode = .never
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
rebuildMenus()
|
||||
|
||||
appServices.permissionContext.onChange = { [weak self] in
|
||||
self?.rebuildMenus()
|
||||
}
|
||||
}
|
||||
|
||||
private func rebuildMenus() {
|
||||
let services = appServices
|
||||
viewModel.buildMenus(
|
||||
from: services.permissionContext.rolePermissions,
|
||||
currentRoleId: services.permissionContext.currentRole?.id
|
||||
)
|
||||
commonURIs = commonMenuStore.load(menuItems: viewModel.menuItems)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
private var commonItems: [HomeMenuItem] {
|
||||
commonURIs.compactMap { menuItem(for: $0) }
|
||||
}
|
||||
|
||||
private var moreItems: [HomeMenuItem] {
|
||||
viewModel.menuItems.filter { !isCommonURI($0.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
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeMoreFunctionsViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int { 2 }
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 }
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
section == 0 ? "常用应用" : "更多功能"
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: HomeMoreMenuGridCell.reuseID, for: indexPath) as! HomeMoreMenuGridCell
|
||||
let isCommon = indexPath.section == 0
|
||||
let items = isCommon ? commonItems : moreItems
|
||||
cell.configure(items: items, isCommon: isCommon) { [weak self] item in
|
||||
self?.openMenu(item)
|
||||
} onToggle: { [weak self] item in
|
||||
self?.toggleCommon(item, isCommon: isCommon)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
let count = indexPath.section == 0 ? commonItems.count : moreItems.count
|
||||
let rows = max(1, Int(ceil(Double(count) / 3.0)))
|
||||
return CGFloat(rows) * 124 + 8
|
||||
}
|
||||
}
|
||||
|
||||
private final class HomeMoreMenuGridCell: UITableViewCell {
|
||||
static let reuseID = "HomeMoreMenuGridCell"
|
||||
|
||||
private var items: [HomeMenuItem] = []
|
||||
private var isCommon = false
|
||||
private var onSelect: ((HomeMenuItem) -> Void)?
|
||||
private var onToggle: ((HomeMenuItem) -> Void)?
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.minimumInteritemSpacing = 14
|
||||
layout.minimumLineSpacing = AppMetrics.Spacing.mediumLarge
|
||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
view.backgroundColor = .clear
|
||||
view.isScrollEnabled = false
|
||||
view.dataSource = self
|
||||
view.delegate = self
|
||||
view.register(HomeMoreMenuItemCell.self, forCellWithReuseIdentifier: HomeMoreMenuItemCell.reuseID)
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
contentView.addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppMetrics.Spacing.mediumLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(
|
||||
items: [HomeMenuItem],
|
||||
isCommon: Bool,
|
||||
onSelect: @escaping (HomeMenuItem) -> Void,
|
||||
onToggle: @escaping (HomeMenuItem) -> Void
|
||||
) {
|
||||
self.items = items
|
||||
self.isCommon = isCommon
|
||||
self.onSelect = onSelect
|
||||
self.onToggle = onToggle
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeMoreMenuGridCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
items.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeMoreMenuItemCell.reuseID, for: indexPath) as! HomeMoreMenuItemCell
|
||||
let item = items[indexPath.item]
|
||||
cell.configure(item: item, isCommon: isCommon) { [weak self] in
|
||||
self?.onToggle?(item)
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
onSelect?(items[indexPath.item])
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let width = (collectionView.bounds.width - 28) / 3
|
||||
return CGSize(width: width, height: 112)
|
||||
}
|
||||
}
|
||||
|
||||
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?()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
//
|
||||
// HomePlaceholderViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页迁移占位页,用于尚未完成 UIKit 迁移的入口。
|
||||
final class HomePlaceholderViewController: UIViewController {
|
||||
private let pageTitle: String
|
||||
private let uri: String?
|
||||
|
||||
init(title: String, uri: String? = nil) {
|
||||
pageTitle = title
|
||||
self.uri = uri
|
||||
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 = pageTitle
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: "square.grid.2x2"))
|
||||
iconView.tintColor = AppDesign.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = pageTitle
|
||||
titleLabel.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
titleLabel.textColor = AppDesign.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
let uriLabel = UILabel()
|
||||
uriLabel.text = uri
|
||||
uriLabel.font = .systemFont(ofSize: 14)
|
||||
uriLabel.textColor = AppDesign.textSecondary
|
||||
uriLabel.textAlignment = .center
|
||||
uriLabel.numberOfLines = 2
|
||||
uriLabel.isHidden = uri?.isEmpty != false
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel, uriLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
stack.alignment = .center
|
||||
view.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.leading.trailing.equalToSuperview().inset(24)
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(44)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,583 @@
|
||||
//
|
||||
// HomeViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 首页工作台,展示景区头部、工作状态、位置上报卡片和常用应用网格。
|
||||
final class HomeViewController: UIViewController {
|
||||
|
||||
private let viewModel = HomeViewModel()
|
||||
private let commonMenuStore = HomeCommonMenuStore()
|
||||
private var commonURIs: [String] = []
|
||||
private var isOnline = false
|
||||
private var reminderMinutes = 0
|
||||
private var secondsUntilReport = 0
|
||||
private var countdownTimer: Timer?
|
||||
|
||||
private let minimalTopRoleIds: Set<Int> = [46, 47, 52, 53, 54]
|
||||
|
||||
private lazy var scenicButton: UIButton = {
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.baseForegroundColor = UIColor(hex: 0x333333)
|
||||
config.image = UIImage(systemName: "chevron.down")?.withConfiguration(
|
||||
UIImage.SymbolConfiguration(pointSize: AppMetrics.FontSize.subheadline, weight: .bold)
|
||||
)
|
||||
config.imagePlacement = .trailing
|
||||
config.imagePadding = AppMetrics.Spacing.xSmall
|
||||
config.contentInsets = .zero
|
||||
let button = UIButton(configuration: config)
|
||||
button.contentHorizontalAlignment = .leading
|
||||
button.addTarget(self, action: #selector(scenicTapped), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let table = UITableView(frame: .zero, style: .grouped)
|
||||
table.backgroundColor = AppDesignUIKit.pageBackground
|
||||
table.separatorStyle = .none
|
||||
table.showsVerticalScrollIndicator = false
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.register(HomeMenuGridCell.self, forCellReuseIdentifier: HomeMenuGridCell.reuseID)
|
||||
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
|
||||
return table
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = AppDesignUIKit.pageBackground
|
||||
setupTopBar()
|
||||
setupTableView()
|
||||
bindViewModel()
|
||||
rebuildMenus()
|
||||
observeContextChanges()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
startCountdownTimerIfNeeded()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
}
|
||||
|
||||
private func setupTopBar() {
|
||||
let topBar = UIView()
|
||||
topBar.backgroundColor = .white
|
||||
view.addSubview(topBar)
|
||||
topBar.addSubview(scenicButton)
|
||||
|
||||
topBar.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.height.equalTo(78)
|
||||
}
|
||||
scenicButton.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
updateScenicTitle()
|
||||
}
|
||||
|
||||
private func setupTableView() {
|
||||
view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide).offset(78)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.onChange = { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private func observeContextChanges() {
|
||||
let services = appServices
|
||||
services.permissionContext.onChange = { [weak self] in
|
||||
self?.rebuildMenus()
|
||||
}
|
||||
services.accountContext.onChange = { [weak self] in
|
||||
self?.updateScenicTitle()
|
||||
self?.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private func rebuildMenus() {
|
||||
let services = appServices
|
||||
viewModel.buildMenus(
|
||||
from: services.permissionContext.rolePermissions,
|
||||
currentRoleId: services.permissionContext.currentRole?.id
|
||||
)
|
||||
commonURIs = commonMenuStore.load(menuItems: viewModel.menuItems)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
private func updateScenicTitle() {
|
||||
let name = appServices.accountContext.currentScenic?.name ?? "请选择景区"
|
||||
scenicButton.configuration?.attributedTitle = AttributedString(
|
||||
name,
|
||||
attributes: AttributeContainer([
|
||||
.font: UIFont.systemFont(ofSize: AppMetrics.FontSize.title3, weight: .semibold)
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
private var currentRoleId: Int? {
|
||||
appServices.permissionContext.currentRole?.id
|
||||
}
|
||||
|
||||
private var shouldShowWorkStatus: Bool {
|
||||
guard let currentRoleId else { return true }
|
||||
return !minimalTopRoleIds.contains(currentRoleId)
|
||||
}
|
||||
|
||||
private var isStoreManager: Bool {
|
||||
currentRoleId == 46
|
||||
}
|
||||
|
||||
private var displayMenuItems: [HomeMenuItem] {
|
||||
let selected = commonURIs.compactMap { menuItem(for: $0) }
|
||||
let fallback = selected.isEmpty ? Array(viewModel.menuItems.prefix(3)) : selected
|
||||
return fallback + [HomeMenuItem(title: "更多功能", uri: "more_functions", iconSrc: nil)]
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private var countdownDisplay: String {
|
||||
let hours = secondsUntilReport / 3_600
|
||||
let minutes = (secondsUntilReport % 3_600) / 60
|
||||
let seconds = secondsUntilReport % 60
|
||||
return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))"
|
||||
}
|
||||
|
||||
private var reminderText: String {
|
||||
reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
|
||||
}
|
||||
|
||||
private func startCountdownTimerIfNeeded() {
|
||||
countdownTimer?.invalidate()
|
||||
guard isOnline else { return }
|
||||
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
guard let self, self.isOnline, self.secondsUntilReport > 0 else { return }
|
||||
self.secondsUntilReport -= 1
|
||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func scenicTapped() {
|
||||
HomeMenuRouting.push(.scenicSelection, from: self)
|
||||
}
|
||||
|
||||
@objc private func onlineTapped() {
|
||||
let message = isOnline
|
||||
? "是否确认切换为离线状态?离线后将暂停位置上报。"
|
||||
: "是否确认切换为在线状态?在线后将开始位置上报和计时。"
|
||||
let alert = UIAlertController(title: "切换在线状态", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.isOnline.toggle()
|
||||
if self.isOnline {
|
||||
self.secondsUntilReport = 7_200
|
||||
self.startCountdownTimerIfNeeded()
|
||||
} else {
|
||||
self.countdownTimer?.invalidate()
|
||||
}
|
||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func reminderTapped() {
|
||||
let sheet = UIAlertController(title: "提前提醒时间", message: nil, preferredStyle: .actionSheet)
|
||||
for minute in [0, 5, 10, 15, 30] {
|
||||
let title = minute == 0 ? "不提醒" : "\(minute)分钟"
|
||||
sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||||
self?.reminderMinutes = minute
|
||||
self?.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
||||
})
|
||||
}
|
||||
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(sheet, animated: true)
|
||||
}
|
||||
|
||||
@objc private func locationReportTapped() {
|
||||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "location_report", title: "位置上报"), from: self)
|
||||
}
|
||||
|
||||
@objc private func paymentTapped() {
|
||||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "payment_collection", title: "立即收款"), from: self)
|
||||
}
|
||||
|
||||
@objc private func taskCreateTapped() {
|
||||
HomeMenuRouting.openRoute(HomeMenuRouter.resolve(uri: "task_create", title: "提交任务"), from: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
extension HomeViewController: UITableViewDataSource, UITableViewDelegate {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
var count = 1
|
||||
if shouldShowWorkStatus { count += 2 }
|
||||
if isStoreManager, appServices.accountContext.currentStore != nil { count += 1 }
|
||||
return count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
if section == tableView.numberOfSections - 1 {
|
||||
return "常用应用"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.section == tableView.numberOfSections - 1 {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: HomeMenuGridCell.reuseID, for: indexPath) as! HomeMenuGridCell
|
||||
cell.configure(items: displayMenuItems) { [weak self] item in
|
||||
self.flatMap { HomeMenuRouting.openMenu(item, from: $0) }
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
||||
cell.selectionStyle = .none
|
||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
if shouldShowWorkStatus {
|
||||
if indexPath.section == 0 {
|
||||
cell.contentView.addSubview(makeStatusCard())
|
||||
} else if indexPath.section == 1 {
|
||||
cell.contentView.addSubview(makeLocationCard())
|
||||
} else if indexPath.section == 2, isStoreManager, let store = appServices.accountContext.currentStore {
|
||||
cell.contentView.addSubview(makeStoreCard(store))
|
||||
} else {
|
||||
cell.contentView.addSubview(makeQuickActionsRow())
|
||||
}
|
||||
} else if isStoreManager, let store = appServices.accountContext.currentStore, indexPath.section == 0 {
|
||||
cell.contentView.addSubview(makeStoreCard(store))
|
||||
}
|
||||
|
||||
cell.contentView.subviews.first?.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(
|
||||
top: AppMetrics.Spacing.xxSmall,
|
||||
left: AppMetrics.Spacing.pageHorizontal,
|
||||
bottom: AppMetrics.Spacing.xxSmall,
|
||||
right: AppMetrics.Spacing.pageHorizontal
|
||||
))
|
||||
}
|
||||
cell.backgroundColor = .clear
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
if indexPath.section == tableView.numberOfSections - 1 { return 240 }
|
||||
if shouldShowWorkStatus {
|
||||
switch indexPath.section {
|
||||
case 0: return 100
|
||||
case 1: return 148
|
||||
case 2 where isStoreManager && appServices.accountContext.currentStore != nil: return 88
|
||||
default: return 118
|
||||
}
|
||||
}
|
||||
if isStoreManager, appServices.accountContext.currentStore != nil, indexPath.section == 0 { return 88 }
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
private func makeStatusCard() -> UIView {
|
||||
let card = makeCardView()
|
||||
|
||||
let onlineButton = UIButton(type: .system)
|
||||
onlineButton.setTitle(isOnline ? "在线" : "离线", for: .normal)
|
||||
onlineButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.caption, weight: .medium)
|
||||
onlineButton.setTitleColor(isOnline ? UIColor(hex: 0xF0FDF4) : UIColor(hex: 0x7B8EAA), for: .normal)
|
||||
onlineButton.backgroundColor = isOnline ? UIColor(hex: 0x22C55E) : UIColor(hex: 0xF4F4F4)
|
||||
onlineButton.layer.cornerRadius = 4
|
||||
onlineButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
|
||||
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
|
||||
|
||||
let clockLabel = UILabel()
|
||||
clockLabel.text = " \(countdownDisplay)"
|
||||
clockLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||||
clockLabel.textColor = AppDesignUIKit.primary
|
||||
|
||||
let reminderButton = UIButton(type: .system)
|
||||
reminderButton.setTitle(" \(reminderText)", for: .normal)
|
||||
reminderButton.titleLabel?.font = .systemFont(ofSize: AppMetrics.FontSize.body, weight: .medium)
|
||||
reminderButton.setTitleColor(AppDesignUIKit.primary, for: .normal)
|
||||
reminderButton.addTarget(self, action: #selector(reminderTapped), for: .touchUpInside)
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [onlineButton, clockLabel, reminderButton])
|
||||
stack.axis = .horizontal
|
||||
stack.distribution = .equalSpacing
|
||||
stack.alignment = .center
|
||||
card.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(15)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeLocationCard() -> UIView {
|
||||
let card = makeCardView()
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = "立即上报"
|
||||
titleLabel.font = .systemFont(ofSize: 22, weight: .bold)
|
||||
titleLabel.textColor = UIColor(hex: 0x333333)
|
||||
|
||||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.text = "您已进入打卡范围"
|
||||
subtitleLabel.font = .systemFont(ofSize: 15)
|
||||
subtitleLabel.textColor = UIColor(hex: 0x999999)
|
||||
|
||||
let textStack = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
textStack.axis = .vertical
|
||||
textStack.spacing = AppMetrics.Spacing.xxSmall
|
||||
|
||||
let actionButton = UIButton(type: .system)
|
||||
actionButton.setImage(UIImage(systemName: "hand.tap.fill"), for: .normal)
|
||||
actionButton.tintColor = .white
|
||||
actionButton.backgroundColor = AppDesignUIKit.primary
|
||||
actionButton.layer.cornerRadius = 46
|
||||
actionButton.addTarget(self, action: #selector(locationReportTapped), for: .touchUpInside)
|
||||
|
||||
card.addSubview(textStack)
|
||||
card.addSubview(actionButton)
|
||||
textStack.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(15)
|
||||
make.trailing.lessThanOrEqualTo(actionButton.snp.leading).offset(-12)
|
||||
}
|
||||
actionButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().inset(15)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(92)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeQuickActionsRow() -> UIView {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = AppMetrics.Spacing.small
|
||||
stack.distribution = .fillEqually
|
||||
|
||||
stack.addArrangedSubview(quickActionButton(icon: "qrcode", title: "立即收款", action: #selector(paymentTapped)))
|
||||
stack.addArrangedSubview(quickActionButton(icon: "checklist.checked", title: "提交任务", action: #selector(taskCreateTapped)))
|
||||
stack.addArrangedSubview(quickActionButton(
|
||||
icon: isOnline ? "wifi" : "wifi.slash",
|
||||
title: isOnline ? "在线" : "离线",
|
||||
action: #selector(onlineTapped),
|
||||
active: isOnline
|
||||
))
|
||||
return stack
|
||||
}
|
||||
|
||||
private func quickActionButton(icon: String, title: String, action: Selector, active: Bool = false) -> UIView {
|
||||
let card = makeCardView()
|
||||
card.backgroundColor = active ? UIColor(hex: 0xE3F2FD) : .white
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: icon))
|
||||
iconView.tintColor = AppDesignUIKit.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = .systemFont(ofSize: AppMetrics.FontSize.body)
|
||||
label.textAlignment = .center
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, label])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppMetrics.Spacing.xSmall
|
||||
stack.alignment = .center
|
||||
card.addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.height.equalTo(34)
|
||||
}
|
||||
|
||||
let button = UIButton(type: .custom)
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
card.addSubview(button)
|
||||
button.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
card.snp.makeConstraints { make in
|
||||
make.height.equalTo(102)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeStoreCard(_ store: BusinessScope) -> UIView {
|
||||
let card = makeCardView()
|
||||
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = store.name
|
||||
nameLabel.font = .systemFont(ofSize: AppMetrics.FontSize.title2, weight: .bold)
|
||||
|
||||
let scenicLabel = UILabel()
|
||||
scenicLabel.text = appServices.accountContext.currentScenic?.name
|
||||
scenicLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
|
||||
scenicLabel.textColor = UIColor(hex: 0x7B8EAA)
|
||||
scenicLabel.numberOfLines = 2
|
||||
|
||||
let statusLabel = UILabel()
|
||||
statusLabel.text = "营业中"
|
||||
statusLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
|
||||
statusLabel.textColor = UIColor(hex: 0x22C55E)
|
||||
statusLabel.backgroundColor = UIColor(hex: 0xF0FDF4)
|
||||
statusLabel.layer.cornerRadius = 4
|
||||
statusLabel.clipsToBounds = true
|
||||
statusLabel.textAlignment = .center
|
||||
|
||||
let textStack = UIStackView(arrangedSubviews: [nameLabel, scenicLabel])
|
||||
textStack.axis = .vertical
|
||||
textStack.spacing = AppMetrics.Spacing.xSmall
|
||||
|
||||
card.addSubview(textStack)
|
||||
card.addSubview(statusLabel)
|
||||
textStack.snp.makeConstraints { make in
|
||||
make.leading.top.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.trailing.lessThanOrEqualTo(statusLabel.snp.leading).offset(-8)
|
||||
}
|
||||
statusLabel.snp.makeConstraints { make in
|
||||
make.trailing.top.equalToSuperview().inset(AppMetrics.Spacing.medium)
|
||||
make.width.greaterThanOrEqualTo(52)
|
||||
make.height.equalTo(22)
|
||||
}
|
||||
return card
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menu Grid Cell
|
||||
|
||||
private final class HomeMenuGridCell: UITableViewCell {
|
||||
static let reuseID = "HomeMenuGridCell"
|
||||
|
||||
private var onSelect: ((HomeMenuItem) -> Void)?
|
||||
private var items: [HomeMenuItem] = []
|
||||
|
||||
private lazy var collectionView: UICollectionView = {
|
||||
let layout = UICollectionViewFlowLayout()
|
||||
layout.minimumInteritemSpacing = 15
|
||||
layout.minimumLineSpacing = 15
|
||||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
view.backgroundColor = .clear
|
||||
view.isScrollEnabled = false
|
||||
view.dataSource = self
|
||||
view.delegate = self
|
||||
view.register(HomeMenuItemCell.self, forCellWithReuseIdentifier: HomeMenuItemCell.reuseID)
|
||||
return view
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
contentView.addSubview(collectionView)
|
||||
collectionView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
|
||||
make.height.equalTo(220)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(items: [HomeMenuItem], onSelect: @escaping (HomeMenuItem) -> Void) {
|
||||
self.items = items
|
||||
self.onSelect = onSelect
|
||||
collectionView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeMenuGridCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
items.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeMenuItemCell.reuseID, for: indexPath) as! HomeMenuItemCell
|
||||
cell.configure(item: items[indexPath.item])
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
onSelect?(items[indexPath.item])
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let width = (collectionView.bounds.width - 30) / 3
|
||||
return CGSize(width: width, height: 102)
|
||||
}
|
||||
}
|
||||
|
||||
private final class HomeMenuItemCell: UICollectionViewCell {
|
||||
static let reuseID = "HomeMenuItemCell"
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.backgroundColor = .white
|
||||
contentView.layer.cornerRadius = 8
|
||||
|
||||
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.body)
|
||||
titleLabel.textColor = UIColor(hex: 0x4B5563)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.numberOfLines = 1
|
||||
titleLabel.adjustsFontSizeToFitWidth = true
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = AppMetrics.Spacing.small
|
||||
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(30)
|
||||
}
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configure(item: HomeMenuItem) {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user