Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -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
)
}
}

View File

@ -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?()
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}