Initial commit

This commit is contained in:
2026-06-22 11:28:01 +08:00
commit ace9c94359
84 changed files with 8899 additions and 0 deletions

View File

@ -0,0 +1,87 @@
//
// HomeIconCatalog.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Foundation
/// URI
enum HomeIconCatalog {
/// URI SF Symbols
static func iconName(for uri: String) -> String {
switch uri {
case "space_settings":
"person.crop.square.fill"
case "album_list":
"photo.stack"
case "album_trailer":
"square.stack.3d.up.fill"
case "wallet":
"creditcard.fill"
case "cloud_management":
"icloud.fill"
case "cloud_storage_transit":
"arrow.left.arrow.right.circle.fill"
case "asset_management", "/scenic-order-manage":
"photo.on.rectangle.angled"
case "material_upload":
"square.and.arrow.up"
case "task_management", "task_management_editor":
"checklist"
case "schedule_management":
"calendar"
case "system_settings":
"gearshape.fill"
case "message_center":
"bell.fill"
case "checkin_points":
"mappin.and.ellipse"
case "sample_management":
"point.3.connected.trianglepath.dotted"
case "sample_upload":
"square.and.arrow.up.on.square"
case "live_stream_management":
"dot.radiowaves.left.and.right"
case "verification_order":
"checkmark.seal.fill"
case "live_album":
"play.rectangle.on.rectangle.fill"
case "pm", "pm_manager", "project_edit":
"circle.grid.2x2.fill"
case "location_report", "location_report_history":
"mappin.circle.fill"
case "registration_invitation", "photographer_invite":
"envelope.open.fill"
case "store":
"storefront.fill"
case "fly", "pilot_cert", "pilot_controller":
"paperplane.fill"
case "/scenic-queue", "queue_management":
"person.3.fill"
case "operating-area":
"map.fill"
case "scenicselection":
"location.magnifyingglass"
case "scenicapplication":
"doc.badge.plus"
case "permission_apply", "permission_apply_status":
"person.badge.key"
case "scenic_settlement", "scenic_settlement_review":
"checklist"
case "payment_collection", "payment_qr", "payment_code":
"qrcode"
case "deposit_order_detail", "deposit_order", "deposit_order_shooting_info":
"doc.text.magnifyingglass"
case "withdrawal_audit":
"banknote.fill"
case "invite_record":
"list.bullet.rectangle.fill"
case "more_functions":
"ellipsis"
default:
"square.grid.2x2.fill"
}
}
}

View File

@ -0,0 +1,213 @@
//
// HomeMoreFunctionsView.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import SwiftUI
///
struct HomeMoreFunctionsView: View {
@Environment(PermissionContext.self) private var permissionContext
@Environment(AppRouter.self) private var appRouter
@Environment(RouterPath.self) private var router
@Environment(\.dismiss) private var dismiss
@State private var viewModel = HomeViewModel()
@State private var commonUris: [String] = []
private let commonMenuStore = HomeCommonMenuStore()
private var commonItems: [HomeMenuItem] {
commonUris.compactMap(menuItem(for:))
}
private var moreItems: [HomeMenuItem] {
viewModel.menuItems.filter { item in
!isCommonURI(item.uri)
}
}
var body: some View {
VStack(spacing: 0) {
topBar
ScrollView {
VStack(alignment: .leading, spacing: 26) {
section(title: "常用应用", items: commonItems, isCommon: true)
section(title: "更多功能", items: moreItems, isCommon: false)
}
.padding(.horizontal, AppMetrics.Spacing.mediumLarge)
.padding(.top, AppMetrics.Spacing.xxLarge + 1)
.padding(.bottom, AppMetrics.Spacing.xxLarge)
}
.background(Color(hex: 0xF5F5F5))
}
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
.toolbar(.hidden, for: .navigationBar)
.task {
rebuildMenus()
}
.onChange(of: permissionContext.currentRole?.id) { _, _ in
rebuildMenus()
}
.onChange(of: permissionContext.rolePermissions.count) { _, _ in
rebuildMenus()
}
}
private var topBar: some View {
ZStack {
Text("全部功能")
.font(.system(size: AppMetrics.FontSize.title2 + 1, weight: .semibold))
.foregroundStyle(Color(hex: 0x2C2C2C))
.frame(maxWidth: .infinity)
HStack {
Button {
dismiss()
} label: {
Image(systemName: "chevron.left")
.font(.system(size: AppMetrics.FontSize.body, weight: .semibold))
.foregroundStyle(Color(hex: 0x111827))
.frame(width: AppMetrics.ControlSize.iconTapArea, height: AppMetrics.ControlSize.iconTapArea)
}
.buttonStyle(.plain)
Spacer()
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
}
.frame(height: 62)
.background(.white)
.overlay(alignment: .bottom) {
Rectangle()
.fill(Color.black.opacity(0.06))
.frame(height: 0.5)
}
}
private func section(title: String, items: [HomeMenuItem], isCommon: Bool) -> some View {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.medium) {
Text(title)
.font(.system(size: AppMetrics.FontSize.title, weight: .bold))
.foregroundStyle(Color(hex: 0x2C2C2C))
appGrid(items: items, isCommon: isCommon)
}
}
private func appGrid(items: [HomeMenuItem], isCommon: Bool) -> some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 14), count: 3), spacing: AppMetrics.Spacing.mediumLarge) {
ForEach(items, id: \.uri) { item in
ZStack(alignment: .topTrailing) {
Button {
openMenu(item)
} label: {
VStack(spacing: AppMetrics.Spacing.mediumLarge + 1) {
menuIconView(for: item)
.frame(width: 34, height: 34)
Text(item.title)
.font(.system(size: AppMetrics.FontSize.callout))
.foregroundStyle(Color(hex: 0x252525))
.lineLimit(1)
.minimumScaleFactor(0.78)
}
.frame(maxWidth: .infinity)
.frame(height: 112)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
Button {
if isCommon {
commonUris = commonMenuStore.remove(item.uri, current: commonUris)
} else {
commonUris = commonMenuStore.add(item.uri, current: commonUris, menuItems: viewModel.menuItems)
}
} label: {
Image(systemName: isCommon ? "minus.circle.fill" : "plus.circle.fill")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(isCommon ? Color(hex: 0xFF1111) : AppDesign.primary)
.background(Color.white, in: Circle())
}
.buttonStyle(.plain)
.offset(x: 9, y: -9)
}
.frame(maxWidth: .infinity)
.frame(height: 112)
}
}
}
@ViewBuilder
private func menuIconView(for item: HomeMenuItem) -> some View {
if let src = item.iconSrc,
let url = URL(string: src),
!src.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image.resizable().scaledToFit()
default:
fallbackMenuIcon(for: item.uri)
}
}
} else {
fallbackMenuIcon(for: item.uri)
}
}
private func fallbackMenuIcon(for uri: String) -> some View {
Image(systemName: HomeIconCatalog.iconName(for: uri))
.resizable()
.scaledToFit()
.foregroundStyle(AppDesign.primary)
.frame(width: 34, height: 34)
}
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 openMenu(_ item: HomeMenuItem) {
switch HomeMenuRouter.resolve(uri: item.uri, title: item.title) {
case .tab(let tab):
appRouter.select(tab)
case .destination(let route):
if route == .moreFunctions {
return
}
router.navigate(to: .home(route))
case .unsupported(let uri, let title, _):
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
case .placeholder(let uri, let title):
HomeRouteDiagnostics.recordUnknown(uri: uri, title: title)
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
}
}
private func rebuildMenus() {
viewModel.buildMenus(
from: permissionContext.rolePermissions,
currentRoleId: permissionContext.currentRole?.id
)
commonUris = commonMenuStore.load(menuItems: viewModel.menuItems)
}
}

View File

@ -0,0 +1,82 @@
//
// HomeSupportViews.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import SwiftUI
extension HomeRoute {
///
@ViewBuilder
var destinationView: some View {
switch self {
case .profileSpace:
ProfileView()
case .scenicSelection:
HomeScenicSelectionView()
case .moreFunctions:
HomeMoreFunctionsView()
case let .modulePlaceholder(uri, title):
HomeMigrationModuleView(title: title, uri: uri)
}
}
}
///
struct HomeMigrationModuleView: View {
let title: String
let uri: String
var body: some View {
VStack(spacing: AppMetrics.Spacing.medium) {
Image(systemName: "square.grid.2x2")
.font(.system(size: 44, weight: .semibold))
.foregroundStyle(AppDesign.primary)
Text(title)
.font(.system(size: AppMetrics.FontSize.title2, weight: .semibold))
.foregroundStyle(AppDesign.textPrimary)
Text(uri)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(AppDesign.textSecondary)
.lineLimit(2)
.multilineTextAlignment(.center)
.padding(.horizontal, AppMetrics.Spacing.large)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(hex: 0xF5F7FA))
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
}
}
///
struct HomeScenicSelectionView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(\.dismiss) private var dismiss
var body: some View {
List(accountContext.scenicScopes) { scenic in
Button {
accountContext.selectScenic(id: scenic.id)
dismiss()
} label: {
HStack {
Text(scenic.name)
.foregroundStyle(AppDesign.textPrimary)
Spacer()
if accountContext.currentScenic?.id == scenic.id {
Image(systemName: "checkmark")
.font(.system(size: AppMetrics.ControlSize.smallIcon, weight: .semibold))
.foregroundStyle(AppDesign.primary)
}
}
}
}
.navigationTitle("景区选择")
.navigationBarTitleDisplayMode(.inline)
}
}

View File

@ -0,0 +1,443 @@
//
// HomeView.swift
// suixinkan
//
// Created by Codex on 2026/6/22.
//
import Combine
import SwiftUI
///
struct HomeView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(PermissionContext.self) private var permissionContext
@Environment(AppRouter.self) private var appRouter
@Environment(RouterPath.self) private var router
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@State private var viewModel = HomeViewModel()
@State private var commonUris: [String] = []
@State private var isOnline = false
@State private var reminderMinutes = 0
@State private var secondsUntilReport = 0
@State private var showOnlineDialog = false
@State private var showReminderDialog = false
@State private var showReportSuccess = false
private let commonMenuStore = HomeCommonMenuStore()
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private let minimalTopRoleIds: Set<Int> = [46, 47, 52, 53, 54]
private var contentMaxWidth: CGFloat {
horizontalSizeClass == .regular ? 760 : .infinity
}
private var currentRoleId: Int? {
permissionContext.currentRole?.id
}
private var isStoreManager: Bool {
currentRoleId == 46
}
private var shouldShowWorkStatus: Bool {
guard let currentRoleId else { return true }
return !minimalTopRoleIds.contains(currentRoleId)
}
private var currentScenicName: String {
accountContext.currentScenic?.name ?? "请选择景区"
}
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 var tileHeight: CGFloat {
horizontalSizeClass == .regular ? 118 : 102
}
private var displayMenuItems: [HomeMenuItem] {
let selected = commonUris.compactMap(menuItemForHomeEntry(uri:))
let fallback = selected.isEmpty ? Array(viewModel.menuItems.prefix(3)) : selected
return fallback + [HomeMenuItem(title: "更多功能", uri: "more_functions", iconSrc: nil)]
}
var body: some View {
VStack(spacing: 0) {
topBar
ScrollView {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.small) {
if shouldShowWorkStatus {
statusCard
locationReportCard
quickActionsRow
}
if isStoreManager, let store = accountContext.currentStore {
storeCard(store)
.padding(.bottom, AppMetrics.Spacing.xSmall)
}
Text("常用应用")
.font(.system(size: AppMetrics.FontSize.body, weight: .bold))
.foregroundStyle(Color(hex: 0x333333))
.padding(.top, shouldShowWorkStatus ? AppMetrics.Spacing.xxSmall : AppMetrics.Spacing.xSmall)
.padding(.bottom, AppMetrics.Spacing.xxSmall)
appGrid
}
.frame(maxWidth: contentMaxWidth, alignment: .topLeading)
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.padding(.top, AppMetrics.Spacing.xxLarge)
.padding(.bottom, AppMetrics.Spacing.pageVertical)
.frame(maxWidth: .infinity)
}
.background(Color(hex: 0xF5F5F5))
}
.background(Color(hex: 0xF5F5F5).ignoresSafeArea())
.toolbar(.hidden, for: .navigationBar)
.task {
rebuildMenusFromCurrentContext()
}
.onReceive(timer) { _ in
guard isOnline, secondsUntilReport > 0 else { return }
secondsUntilReport -= 1
}
.onChange(of: permissionContext.currentRole?.id) { _, _ in
rebuildMenusFromCurrentContext()
}
.onChange(of: permissionContext.rolePermissions.count) { _, _ in
rebuildMenusFromCurrentContext()
}
.alert("切换在线状态", isPresented: $showOnlineDialog) {
Button("取消", role: .cancel) {}
Button("确定") {
isOnline.toggle()
if isOnline {
secondsUntilReport = 7_200
}
}
} message: {
Text(isOnline ? "是否确认切换为离线状态?离线后将暂停位置上报。" : "是否确认切换为在线状态?在线后将开始位置上报和计时。")
}
.confirmationDialog("提前提醒时间", isPresented: $showReminderDialog, titleVisibility: .visible) {
ForEach([0, 5, 10, 15, 30], id: \.self) { minute in
Button(minute == 0 ? "不提醒" : "\(minute)分钟") {
reminderMinutes = minute
}
}
Button("取消", role: .cancel) {}
}
.alert("上报成功", isPresented: $showReportSuccess) {
Button("知道了", role: .cancel) {}
} message: {
Text("上报已成功,距离下次上报时间 \(countdownDisplay)")
}
}
private var topBar: some View {
HStack {
Button {
router.navigate(to: .home(.scenicSelection))
} label: {
HStack(spacing: AppMetrics.Spacing.xSmall) {
Text(currentScenicName)
.font(.system(size: AppMetrics.FontSize.title3, weight: .semibold))
.foregroundStyle(Color(hex: 0x333333))
.lineLimit(1)
.minimumScaleFactor(0.8)
Image(systemName: "chevron.down")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .bold))
.foregroundStyle(.black)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
.padding(.horizontal, AppMetrics.Spacing.pageHorizontal)
.frame(height: 78)
.background(.white)
}
private var statusCard: some View {
HStack(spacing: AppMetrics.Spacing.small) {
Button {
showOnlineDialog = true
} label: {
Text(isOnline ? "在线" : "离线")
.font(.system(size: AppMetrics.FontSize.caption, weight: .medium))
.foregroundStyle(isOnline ? Color(hex: 0xF0FDF4) : Color(hex: 0x7B8EAA))
.padding(.horizontal, AppMetrics.Spacing.small)
.padding(.vertical, AppMetrics.Spacing.xxSmall + 2)
.background(isOnline ? Color(hex: 0x22C55E) : Color(hex: 0xF4F4F4), in: RoundedRectangle(cornerRadius: 4))
}
.buttonStyle(.plain)
Spacer(minLength: AppMetrics.Spacing.xxSmall)
Label {
Text(countdownDisplay)
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
} icon: {
Image(systemName: "clock")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium))
}
.foregroundStyle(AppDesign.primary)
.lineLimit(1)
.minimumScaleFactor(0.85)
Spacer(minLength: AppMetrics.Spacing.xxSmall)
Button {
showReminderDialog = true
} label: {
Label {
Text(reminderText)
.font(.system(size: AppMetrics.FontSize.body, weight: .medium))
} icon: {
Image(systemName: "bell.fill")
.font(.system(size: AppMetrics.FontSize.subheadline, weight: .medium))
}
.foregroundStyle(AppDesign.primary)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.buttonStyle(.plain)
}
.padding(15)
.frame(minHeight: 84)
.frame(maxWidth: .infinity)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
private var locationReportCard: some View {
HStack(spacing: AppMetrics.Spacing.medium) {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xxSmall) {
HStack(spacing: AppMetrics.Spacing.xSmall) {
Image(systemName: "arrow.up.square.fill")
.font(.system(size: 25, weight: .bold))
.foregroundStyle(AppDesign.primary)
Text("立即上报")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(Color(hex: 0x333333))
}
Text("您已进入打卡范围")
.font(.system(size: 15))
.foregroundStyle(Color(hex: 0x999999))
}
Spacer()
Button {
secondsUntilReport = 7_200
showReportSuccess = true
} label: {
Image(systemName: "hand.tap.fill")
.font(.system(size: 38, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 92, height: 92)
.background(
LinearGradient(
colors: [Color(hex: 0x0073FF), Color(hex: 0x5CA8FF)],
startPoint: .top,
endPoint: .bottom
),
in: Circle()
)
}
.buttonStyle(.plain)
.accessibilityLabel("上报位置")
}
.padding(15)
.frame(minHeight: 132)
.frame(maxWidth: .infinity)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
private var quickActionsRow: some View {
HStack(spacing: AppMetrics.Spacing.small) {
quickAction(icon: "qrcode", title: "立即收款") {
openRoute(HomeMenuRouter.resolve(uri: "payment_collection", title: "立即收款"))
}
quickAction(icon: "checklist.checked", title: "提交任务") {
openRoute(HomeMenuRouter.resolve(uri: "task_create", title: "提交任务"))
}
quickAction(icon: isOnline ? "wifi" : "wifi.slash", title: isOnline ? "在线" : "离线", active: isOnline) {
showOnlineDialog = true
}
}
}
private func quickAction(icon: String, title: String, active: Bool = false, action: @escaping () -> Void) -> some View {
Button(action: action) {
VStack(spacing: AppMetrics.Spacing.xSmall) {
Image(systemName: icon)
.font(.system(size: 30, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(height: 34)
Text(title)
.font(.system(size: AppMetrics.FontSize.body))
.foregroundStyle(Color.black)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity)
.frame(height: tileHeight)
.background(active ? Color(hex: 0xE3F2FD) : .white, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
}
private func storeCard(_ store: BusinessScope) -> some View {
HStack(alignment: .top, spacing: 14) {
VStack(alignment: .leading, spacing: AppMetrics.Spacing.xSmall) {
Text(store.name)
.font(.system(size: AppMetrics.FontSize.title2, weight: .bold))
.foregroundStyle(.black)
.lineLimit(1)
if let scenic = accountContext.currentScenic {
Text(scenic.name)
.font(.system(size: AppMetrics.FontSize.subheadline))
.foregroundStyle(Color(hex: 0x7B8EAA))
.lineLimit(2)
}
}
Spacer()
Text("营业中")
.font(.system(size: AppMetrics.FontSize.caption))
.foregroundStyle(Color(hex: 0x22C55E))
.padding(.horizontal, AppMetrics.Spacing.xSmall + 2)
.padding(.vertical, AppMetrics.Spacing.xxxSmall)
.background(Color(hex: 0xF0FDF4), in: RoundedRectangle(cornerRadius: 4))
}
.padding(AppMetrics.Spacing.medium)
.frame(maxWidth: .infinity)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
private var appGrid: some View {
LazyVGrid(columns: menuColumns, spacing: 15) {
ForEach(displayMenuItems) { item in
Button {
openMenu(item)
} label: {
VStack(spacing: AppMetrics.Spacing.small) {
menuIconView(for: item)
.frame(width: 30, height: 30)
Text(item.title)
.font(.system(size: AppMetrics.FontSize.body))
.foregroundStyle(Color(hex: 0x4B5563))
.lineLimit(1)
.minimumScaleFactor(0.75)
}
.frame(maxWidth: .infinity)
.frame(height: tileHeight)
.background(.white, in: RoundedRectangle(cornerRadius: 8))
}
.buttonStyle(.plain)
}
}
}
private var menuColumns: [GridItem] {
Array(repeating: GridItem(.flexible(), spacing: 15), count: 3)
}
@ViewBuilder
private func menuIconView(for item: HomeMenuItem) -> some View {
if let src = item.iconSrc,
let url = URL(string: src),
!src.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.scaledToFit()
default:
fallbackMenuIcon(for: item.uri)
}
}
} else {
fallbackMenuIcon(for: item.uri)
}
}
private func fallbackMenuIcon(for uri: String) -> some View {
Image(systemName: iconName(for: uri))
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(AppDesign.primary)
.frame(width: 30, height: 30)
}
private func iconName(for uri: String) -> String {
HomeIconCatalog.iconName(for: uri)
}
private func menuItemForHomeEntry(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 openMenu(_ item: HomeMenuItem) {
openRoute(HomeMenuRouter.resolve(uri: item.uri, title: item.title))
}
private func openRoute(_ route: HomeMenuResolvedRoute) {
switch route {
case .tab(let tab):
appRouter.select(tab)
case .destination(let route):
router.navigate(to: .home(route))
case .unsupported(let uri, let title, _):
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
case .placeholder(let uri, let title):
HomeRouteDiagnostics.recordUnknown(uri: uri, title: title)
router.navigate(to: .home(.modulePlaceholder(uri: uri, title: title)))
}
}
private func rebuildMenusFromCurrentContext() {
viewModel.buildMenus(
from: permissionContext.rolePermissions,
currentRoleId: permissionContext.currentRole?.id
)
commonUris = commonMenuStore.load(menuItems: viewModel.menuItems)
}
}
#Preview {
NavigationStack {
HomeView()
.environment(AccountContext())
.environment(PermissionContext())
.environment(AppRouter())
.environment(RouterPath())
}
}