Align iOS home common apps and all-functions page with Android.

Add unified HomeCommonMenu/HomeAllFunctions diagnostics, rebuild all-functions from top-level menuList permissions, and document Home and AllFunctions module logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-30 09:40:02 +08:00
parent 63fb0462d6
commit 5692134efc
11 changed files with 693 additions and 138 deletions

View File

@ -74,13 +74,17 @@ final class PermissionContext: ObservableObject {
/// URI API
func topLevelPermissionURIs(for roleCode: String?) -> [String] {
topLevelPermissions(for: roleCode).map(\.uri)
}
/// API
func topLevelPermissions(for roleCode: String?) -> [PermissionItem] {
guard let roleCode,
let rolePermission = matchedRolePermission(roleCode: roleCode) else {
return []
}
return rolePermission.role.permission.compactMap { item in
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
return uri.isEmpty ? nil : uri
return rolePermission.role.permission.filter { item in
!item.uri.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}

View File

@ -0,0 +1,209 @@
# 「全部功能」页业务逻辑
Android 工程对应文档:[`zhiflyfollow/docs/home/AllFunctions.md`](../../../zhiflyfollow/docs/home/AllFunctions.md)
## 模块职责
「全部功能」页(`HomeMoreFunctionsView`)展示当前角色可用的功能入口,分为两个分区:
- **常用应用**:已加入首页常用的入口,支持移除(`-`
- **更多功能**:其余可用入口,支持添加为常用(`+`
常用应用的增删会同步写入本地持久化,并与首页「常用应用」网格共用同一套配置(`HomeCommonMenuStore`)。
本页数据构建规则与 Android `AllFunctionsViewModel` 对齐,**不**复用首页 `HomeViewModel` 的 flatten + `preferredOrder` 逻辑。
---
## 涉及文件
| 文件 | 职责 |
| --- | --- |
| `Views/HomeMoreFunctionsView.swift` | 页面 UI、增删常用、触发重建 |
| `Services/HomeAllFunctionsBuilder.swift` | 菜单列表构建与常用/更多拆分 |
| `Services/HomeCommonMenuStore.swift` | 常用 URI 持久化与默认生成 |
| `Services/HomeAllFunctionsDiagnostics.swift` | Debug 诊断日志 |
| `App/State/PermissionContext.swift` | 提供当前角色顶层权限 |
| `Routing/HomeMenuRouter.swift` | 点击入口后的路由解析 |
---
## 数据流
```mermaid
flowchart TD
API[role-permission 接口] --> PC[PermissionContext]
PC --> TLP[topLevelPermissions 顶层节点]
TLP --> BUILDER[HomeAllFunctionsBuilder.build]
CMS[HomeCommonMenuStore.load] --> COMMON[commonURIs]
COMMON --> BUILDER
BUILDER --> ALL[allFunctions]
BUILDER --> CF[commonFunctions]
BUILDER --> MF[moreFunctions]
CF --> UI1[常用应用网格]
MF --> UI2[更多功能网格]
```
### 触发重建的时机
`HomeMoreFunctionsView.rebuildMenus()` 在以下情况执行:
1. 页面首次进入(`.task`
2. 当前角色 `roleCode` 变化
3. `rolePermissions` 数量变化
用户点击 `+` / `-` 时只更新 `commonUris` 并调用 `applySnapshot`,不重新走持久化读取(除非权限上下文同时变化)。
---
## 第一步:读取顶层权限
数据来源:`PermissionContext.topLevelPermissions(for: roleCode)`
- 仅取当前匹配角色的 `role.permission` **顶层数组**
- **不**递归展开 `children` 子节点
- 顺序与 API 返回一致,不做本地重排
- 过滤掉 URI 为空的节点
对应 Android`appStore.getPermission()` 中保存的顶层权限列表MMKV 快照)。
---
## 第二步:白名单过滤(可用入口)
`HomeAllFunctionsBuilder.allMenuItems(from:)` 执行:
```
allFunctions = 顶层权限
.按 API 顺序遍历
.保留 uri ∈ HomeCommonMenuStore.androidHomeMenuURIs 的项
.映射为 HomeMenuItem
```
`androidHomeMenuURIs` 与 Android `Constants.menuList` 登记 URI 一致。以下典型 URI **不会**出现在「全部功能」页,即使接口返回了顶层权限:
- `basic_info``photographer_stats``photographer_orders`
- `location_info``scan_qr``payment_qr``travel_album`
- `album_list``material_upload` 等未登记 URI
子权限 URI 也不会出现(未 flatten
---
## 第三步:菜单项字段映射
每个保留的 `PermissionItem` 转为 `HomeMenuItem`
| 字段 | 规则 |
| --- | --- |
| `uri` | 权限节点原始 URI精确字符串 |
| `title` | API `name` 非空时用 `name`;否则 `HomeMenuRouter.title(for:)`;再经 `HomeMenuRouter.displayTitle` 统一部分同义入口文案 |
| `iconSrc` | API `icon_src`;为空时 UI 层用 `HomeIconCatalog` SF Symbol 兜底 |
Android 端标题/icon 来自本地 `Constants.menuList` drawableiOS 优先接口字段 + 本地图标兜底。
---
## 第四步:读取常用 URI
`HomeCommonMenuStore.load` 提供 `commonUris`,规则详见 [`Home.md`](Home.md)「常用应用」章节,核心要点:
- 存储 key`home.common.menu.uris.account.{accountScope}.role.{roleCode}`(无 accountScope 时用 `home.common.menu.uris.role.{roleCode}`
- 首次无 saved按顶层前 4 个 URI 与 menuList 求交生成默认,并落库
- 已有 saved精确 URI 匹配当前可用顶层权限;全部失效时返回空,不回退默认
- 增删常用:精确 URI 匹配(不使用 `menuAliasKey`
---
## 第五步:拆分为常用 / 更多
`HomeAllFunctionsBuilder.build(topLevelPermissions:commonURIs:)`
```text
commonSet = Set(commonURIs)
commonFunctions = allFunctions.filter { commonSet.contains($0.uri) }
moreFunctions = allFunctions.filter { !commonSet.contains($0.uri) }
```
要点:
- 匹配方式:**精确 URI**,不做别名归一
- **常用区顺序**:跟随 `allFunctions` 列表顺序API 顺序 ∩ 白名单后的顺序),**不是** `commonUris` 数组的存储顺序
- **更多区顺序**:同样跟随 `allFunctions` 剩余项顺序
与 Android 一致:
```kotlin
val common = functions.filter { it.uri in commonUris }
val more = functions.filter { it.uri !in commonUris }
```
---
## 展示层
### 布局
- 两节标题:「常用应用」「更多功能」
- 每节 3 列 `LazyVGrid`,卡片高度 112pt
- 卡片右上角:`常用` 显示红色 `-``更多` 显示蓝色 `+`
### 点击行为
- 点击卡片主体:`HomeMenuRouter.resolve(uri:title:)` 解析路由
- Tab 切换、订单 Tab、Home 子路由、占位页等
- `more_functions` URI 在页内忽略(防循环)
- 点击 `-``HomeCommonMenuStore.remove``applySnapshot`
- 点击 `+``HomeCommonMenuStore.add`(校验 URI 在顶层可用白名单内)→ `applySnapshot`
### 与首页的关系
| 维度 | 首页常用应用网格 | 全部功能页 |
| --- | --- | --- |
| 数据源 | `HomeCommonMenuStore` + `HomeViewModel`(展示标题/icon | `HomeAllFunctionsBuilder` |
| 列表范围 | 仅 common URIs + 「更多功能」入口 | 全部可用入口分 common / more |
| 排序 | common 按 store 顺序映射 | common/more 均按 `allFunctions` API 顺序 |
| 持久化 | 共用 `HomeCommonMenuStore` | 共用 `HomeCommonMenuStore` |
---
## 与 HomeViewModel 的区别
| | `HomeViewModel`(首页等) | `HomeAllFunctionsBuilder`(全部功能) |
| --- | --- | --- |
| 权限范围 | 递归 flatten 整棵权限树 | 仅顶层 |
| 白名单 | 无 | `androidHomeMenuURIs` |
| 排序 | `preferredOrder` 硬编码权重 | API 顶层顺序 |
| 去重 | `menuAliasKey` 别名去重 | 无(顶层 URI 精确保留) |
「全部功能」页**不应**调用 `HomeViewModel.buildMenus()`
---
## 诊断日志
Debug 构建下可用 Xcode Console 过滤 `HomeAllFunctions`
| step | 含义 |
| --- | --- |
| `allFunctions` | 白名单过滤后的完整 URI 列表 |
| `commonFunctions` | 常用分区 URI |
| `moreFunctions` | 更多分区 URI |
常用应用持久化链路仍使用 `HomeCommonMenu` tag`HomeCommonMenuStore` / 首页 `HomeView` 日志。
---
## 测试
单元测试:`suixinkanTests/HomeAllFunctionsBuilderTests.swift`
覆盖场景:
- menuList 白名单过滤
- API 顶层顺序保留(非 preferredOrder
- 不展开子权限
- 精确 URI 拆分 common / more
- 常用区顺序跟随 `allFunctions`
- 与 Android 样例账号27 顶层 → 20 可用 → 3 常用 + 17 更多)一致

View File

@ -1,5 +1,7 @@
# Home 模块业务逻辑
Android 工程对应文档:[`zhiflyfollow/docs/home/Home.md`](../../../zhiflyfollow/docs/home/Home.md)
## 模块职责
Home 模块负责登录后的首页工作台,包括当前景区展示、工作状态、位置上报入口、快捷操作、常用应用和全部功能入口。
@ -18,14 +20,15 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
## 常用应用
`HomeCommonMenuStore` 使用 UserDefaults 按账号和角色保存常用应用 URI
`HomeCommonMenuStore` 使用 UserDefaults 按账号和角色保存常用应用 URI,算法与 Android `AllFunctionsViewModel` 对齐
- 当前版本使用账号 + 角色作用域保存常用应用 URI避免不同业务账号的同一角色互相覆盖旧版角色 key 会在首次读取时迁移。
- 首次无配置时,按 Android 规则从当前角色顶层权限 URI 生成默认值:权限多于 4 个取前 4 个,否则全部使用
-页常用应用按 Android `Constants.menuList` 白名单过滤iOS 已接入但 Android 首页未登记的 URI 不进入首页常用应用
- 读取时按当前角色权限过滤不可用 URI
- 添加移除时按同义 URI 去重
- 可用菜单范围:仅**顶层权限** ∩ Android `Constants.menuList` 白名单(`androidHomeMenuURIs`),不使用扁平化子权限
-次无配置时:先取顶层权限前 4 个 URI原始 API 顺序),再与 menuList 白名单求交后落库
- 已有 saved 配置时:精确 URI 匹配过滤;全部失效时返回空,**不回退**默认生成
- 添加/移除常用应用使用精确 URI 匹配(与 Android 一致)
- 不同角色的常用应用互不影响。
- 升级时会从旧版 `role.{roleId}` key 一次性迁移到 `role.{roleCode}`
- Debug 构建下可通过 `HomeCommonMenu` 日志 tag 与 Android 端逐步对比(`context` / `rawTopLevel` / `availableFunctions` / `savedCommon` / `defaultCommon` / `finalCommon` / `displayItems`)。
## 路由规则
@ -39,6 +42,10 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
`HomeMoreFunctionsView` 使用系统导航栏展示标题和返回,以支持左边缘滑动返回。
## 全部功能
「全部功能」页的数据、筛选、展示逻辑见独立文档 [`AllFunctions.md`](AllFunctions.md)。
## 位置上报
首页工作状态区域由 `HomeLocationViewModel` 驱动,与 `LocationReport` 详情页共享同一实例(在 `RootView` 注入)。

View File

@ -0,0 +1,57 @@
//
// HomeAllFunctionsBuilder.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
/// URI
struct HomeAllFunctionsSnapshot: Equatable {
let allFunctions: [HomeMenuItem]
let commonFunctions: [HomeMenuItem]
let moreFunctions: [HomeMenuItem]
}
/// Android `AllFunctionsViewModel`
enum HomeAllFunctionsBuilder {
/// URI /
static func build(
topLevelPermissions: [PermissionItem],
commonURIs: [String]
) -> HomeAllFunctionsSnapshot {
let allFunctions = allMenuItems(from: topLevelPermissions)
let commonSet = Set(commonURIs)
let commonFunctions = allFunctions.filter { commonSet.contains($0.uri) }
let moreFunctions = allFunctions.filter { !commonSet.contains($0.uri) }
HomeAllFunctionsDiagnostics.log(snapshot: HomeAllFunctionsSnapshot(
allFunctions: allFunctions,
commonFunctions: commonFunctions,
moreFunctions: moreFunctions
))
return HomeAllFunctionsSnapshot(
allFunctions: allFunctions,
commonFunctions: commonFunctions,
moreFunctions: moreFunctions
)
}
/// menuList API
private static func allMenuItems(from topLevelPermissions: [PermissionItem]) -> [HomeMenuItem] {
topLevelPermissions.compactMap { item -> HomeMenuItem? in
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !uri.isEmpty, HomeCommonMenuStore.androidHomeMenuURIs.contains(uri) else {
return nil
}
let fallbackTitle = item.name.isEmpty ? HomeMenuRouter.title(for: uri) : item.name
return HomeMenuItem(
title: HomeMenuRouter.displayTitle(for: uri, fallback: fallbackTitle),
uri: uri,
iconSrc: item.iconSrc
)
}
}
}

View File

@ -0,0 +1,29 @@
//
// HomeAllFunctionsDiagnostics.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
import os
/// 便 Android `AllFunctionsViewModel`
enum HomeAllFunctionsDiagnostics {
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeAllFunctions")
/// URI
static func log(snapshot: HomeAllFunctionsSnapshot) {
#if DEBUG
log(step: "allFunctions", uris: snapshot.allFunctions.map(\.uri))
log(step: "commonFunctions", uris: snapshot.commonFunctions.map(\.uri))
log(step: "moreFunctions", uris: snapshot.moreFunctions.map(\.uri))
#endif
}
/// URI
private static func log(step: String, uris: [String]) {
let payload = "count=\(uris.count) uris=\(HomeCommonMenuDiagnostics.formatURIList(uris))"
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
}
}

View File

@ -0,0 +1,33 @@
//
// HomeCommonMenuDiagnostics.swift
// suixinkan
//
// Created by Codex on 2026/6/30.
//
import Foundation
import os
/// Android `HomeCommonMenu` tag
enum HomeCommonMenuDiagnostics {
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeCommonMenu")
/// 线
static func log(step: String, fields: [String: String]) {
#if DEBUG
let payload = fields.map { "\($0.key)=\($0.value)" }.joined(separator: " ")
logger.debug("step=\(step, privacy: .public) \(payload, privacy: .public)")
#endif
}
/// URI
static func formatURIList(_ uris: [String]) -> String {
"[\(uris.joined(separator: ","))]"
}
/// title|uri
static func formatDisplayItems(_ items: [(title: String, uri: String)]) -> String {
let labels = items.map { "\($0.title)|\($0.uri)" }
return "[\(labels.joined(separator: ","))]"
}
}

View File

@ -49,25 +49,26 @@ struct HomeCommonMenuStore {
self.defaults = defaults
}
/// Android URI 4 4
static func defaultCommonURIs(
orderedTopLevelURIs: [String],
availableURIs: Set<String>
) -> [String] {
let resolved = orderedTopLevelURIs.compactMap { uri -> String? in
/// Android URI 4 URI menuList
static func defaultCommonURIs(orderedTopLevelURIs: [String]) -> [String] {
let rawTopLevel = orderedTopLevelURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let canonical = HomeMenuRouter.canonicalURI(for: trimmed, availableURIs: availableURIs)
return Self.isAndroidHomeMenuURI(canonical) && availableURIs.contains(canonical) ? canonical : nil
return trimmed.isEmpty ? nil : trimmed
}
let deduplicated = unique(resolved)
if deduplicated.count > 4 {
return Array(deduplicated.prefix(4))
}
return deduplicated
let candidates = rawTopLevel.count > 4 ? Array(rawTopLevel.prefix(4)) : rawTopLevel
return candidates.filter { isAndroidHomeMenuURI($0) }
}
/// URI Android
/// Android menuList URI API
static func availableTopLevelMenuURIs(from orderedTopLevelURIs: [String]) -> [String] {
orderedTopLevelURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, isAndroidHomeMenuURI(trimmed) else { return nil }
return trimmed
}
}
/// URI saved 退
func load(
menuItems: [HomeMenuItem],
roleCode: String?,
@ -78,55 +79,99 @@ struct HomeCommonMenuStore {
guard let roleCode, !roleCode.isEmpty else { return [] }
migrateLegacyStorageIfNeeded(roleCode: roleCode, accountScope: accountScope, legacyRoleId: legacyRoleId)
let availableURIs = Set(menuItems.map(\.uri))
let rawTopLevel = topLevelPermissionURIs.compactMap { uri -> String? in
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
let availableTopLevel = Self.availableTopLevelMenuURIs(from: rawTopLevel)
let availableTopLevelSet = Set(availableTopLevel)
let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
let saved = defaults.stringArray(forKey: key)
let saved = defaults.stringArray(forKey: key) ?? []
let defaultCommon = Self.defaultCommonURIs(orderedTopLevelURIs: rawTopLevel)
if let saved, !saved.isEmpty {
let filtered = Self.unique(saved.compactMap { uri -> String? in
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
return Self.isAndroidHomeMenuURI(resolved) && availableURIs.contains(resolved) ? resolved : nil
})
if !filtered.isEmpty {
save(filtered, roleCode: roleCode, accountScope: accountScope)
return filtered
}
HomeCommonMenuDiagnostics.log(
step: "context",
fields: [
"roleCode": roleCode,
"accountScope": accountScope ?? "",
"storageKey": key
]
)
HomeCommonMenuDiagnostics.log(
step: "rawTopLevel",
fields: [
"count": String(rawTopLevel.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(rawTopLevel)
]
)
HomeCommonMenuDiagnostics.log(
step: "availableFunctions",
fields: [
"count": String(availableTopLevel.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(availableTopLevel)
]
)
HomeCommonMenuDiagnostics.log(
step: "savedCommon",
fields: [
"uris": HomeCommonMenuDiagnostics.formatURIList(saved)
]
)
HomeCommonMenuDiagnostics.log(
step: "defaultCommon",
fields: [
"uris": HomeCommonMenuDiagnostics.formatURIList(defaultCommon)
]
)
let finalCommon: [String]
if !saved.isEmpty {
finalCommon = saved.filter { availableTopLevelSet.contains($0) }
} else {
finalCommon = defaultCommon
save(finalCommon, roleCode: roleCode, accountScope: accountScope)
}
let defaultURIs = Self.defaultCommonURIs(
orderedTopLevelURIs: topLevelPermissionURIs,
availableURIs: availableURIs
HomeCommonMenuDiagnostics.log(
step: "finalCommon",
fields: [
"count": String(finalCommon.count),
"uris": HomeCommonMenuDiagnostics.formatURIList(finalCommon)
]
)
save(defaultURIs, roleCode: roleCode, accountScope: accountScope)
return defaultURIs
_ = menuItems
return finalCommon
}
/// URI
func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], roleCode: String?, accountScope: String? = nil) -> [String] {
/// URI 使 URI
func add(
_ uri: String,
current: [String],
topLevelPermissionURIs: [String],
roleCode: String?,
accountScope: String? = nil
) -> [String] {
guard let roleCode, !roleCode.isEmpty else { return current }
let availableURIs = Set(menuItems.map(\.uri))
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
guard Self.isAndroidHomeMenuURI(resolved), availableURIs.contains(resolved) else { return current }
guard !current.contains(where: { HomeMenuRouter.menuAliasKey(for: $0) == HomeMenuRouter.menuAliasKey(for: resolved) }) else {
return current
}
let next = current + [resolved]
let availableTopLevel = Set(Self.availableTopLevelMenuURIs(from: topLevelPermissionURIs))
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
guard availableTopLevel.contains(trimmed), !current.contains(trimmed) else { return current }
let next = current + [trimmed]
save(next, roleCode: roleCode, accountScope: accountScope)
return next
}
/// URI URI
/// URI使 URI
func remove(_ uri: String, current: [String], roleCode: String?, accountScope: String? = nil) -> [String] {
guard let roleCode, !roleCode.isEmpty else { return current }
let aliasKey = HomeMenuRouter.menuAliasKey(for: uri)
let next = current.filter { HomeMenuRouter.menuAliasKey(for: $0) != aliasKey }
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
let next = current.filter { $0 != trimmed }
save(next, roleCode: roleCode, accountScope: accountScope)
return next
}
/// URI
func save(_ uris: [String], roleCode: String, accountScope: String? = nil) {
defaults.set(Self.unique(uris), forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
defaults.set(uris, forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
}
/// key roleCode legacy role id key
@ -170,12 +215,4 @@ struct HomeCommonMenuStore {
private static func isAndroidHomeMenuURI(_ uri: String) -> Bool {
androidHomeMenuURIs.contains(uri)
}
/// URI URI
private static func unique(_ uris: [String]) -> [String] {
var seen = Set<String>()
return uris.filter { uri in
seen.insert(HomeMenuRouter.menuAliasKey(for: uri)).inserted
}
}
}

View File

@ -14,26 +14,20 @@ struct HomeMoreFunctionsView: View {
@EnvironmentObject private var appRouter: AppRouter
@EnvironmentObject private var router: RouterPath
@StateObject private var viewModel = HomeViewModel()
@State private var snapshot = HomeAllFunctionsSnapshot(
allFunctions: [],
commonFunctions: [],
moreFunctions: []
)
@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 {
ScrollView {
VStack(alignment: .leading, spacing: 26) {
section(title: "常用应用", items: commonItems, isCommon: true)
section(title: "更多功能", items: moreItems, isCommon: false)
section(title: "常用应用", items: snapshot.commonFunctions, isCommon: true)
section(title: "更多功能", items: snapshot.moreFunctions, isCommon: false)
}
.padding(.horizontal, AppMetrics.Spacing.mediumLarge)
.padding(.top, AppMetrics.Spacing.medium)
@ -87,23 +81,7 @@ struct HomeMoreFunctionsView: View {
.buttonStyle(.plain)
Button {
let roleCode = permissionContext.currentAppRole?.rawValue
if isCommon {
commonUris = commonMenuStore.remove(
item.uri,
current: commonUris,
roleCode: roleCode,
accountScope: accountContext.accountCachePrefix
)
} else {
commonUris = commonMenuStore.add(
item.uri,
current: commonUris,
menuItems: viewModel.menuItems,
roleCode: roleCode,
accountScope: accountContext.accountCachePrefix
)
}
updateCommonUris(isCommon: isCommon, uri: item.uri)
} label: {
Image(systemName: isCommon ? "minus.circle.fill" : "plus.circle.fill")
.font(.system(size: 24, weight: .bold))
@ -134,24 +112,6 @@ struct HomeMoreFunctionsView: View {
.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):
@ -174,19 +134,49 @@ struct HomeMoreFunctionsView: View {
}
}
///
private func updateCommonUris(isCommon: Bool, uri: String) {
let roleCode = permissionContext.currentAppRole?.rawValue
let topLevelURIs = permissionContext.topLevelPermissionURIs(for: roleCode)
if isCommon {
commonUris = commonMenuStore.remove(
uri,
current: commonUris,
roleCode: roleCode,
accountScope: accountContext.accountCachePrefix
)
} else {
commonUris = commonMenuStore.add(
uri,
current: commonUris,
topLevelPermissionURIs: topLevelURIs,
roleCode: roleCode,
accountScope: accountContext.accountCachePrefix
)
}
applySnapshot(commonURIs: commonUris)
}
private func rebuildMenus() {
let roleCode = permissionContext.currentAppRole?.rawValue
let legacyRoleId = permissionContext.currentRole?.id
viewModel.buildMenus(
from: permissionContext.rolePermissions,
currentRoleCode: roleCode
)
let topLevelPermissions = permissionContext.topLevelPermissions(for: roleCode)
commonUris = commonMenuStore.load(
menuItems: viewModel.menuItems,
menuItems: [],
roleCode: roleCode,
accountScope: accountContext.accountCachePrefix,
legacyRoleId: legacyRoleId,
topLevelPermissionURIs: permissionContext.topLevelPermissionURIs(for: roleCode)
topLevelPermissionURIs: topLevelPermissions.map(\.uri)
)
applySnapshot(commonURIs: commonUris)
}
/// URI
private func applySnapshot(commonURIs: [String]) {
let roleCode = permissionContext.currentAppRole?.rawValue
snapshot = HomeAllFunctionsBuilder.build(
topLevelPermissions: permissionContext.topLevelPermissions(for: roleCode),
commonURIs: commonURIs
)
}
}

View File

@ -456,6 +456,13 @@ struct HomeView: View {
legacyRoleId: legacyRoleId,
topLevelPermissionURIs: permissionContext.topLevelPermissionURIs(for: roleCode)
)
let displayItems = displayMenuItems.map { ($0.title, $0.uri) }
HomeCommonMenuDiagnostics.log(
step: "displayItems",
fields: [
"items": HomeCommonMenuDiagnostics.formatDisplayItems(displayItems)
]
)
}
/// GPS Loading