新增门店订单与核销管理列表,对齐 Android 订单能力并完善账号级缓存。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -18,8 +18,10 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
|
||||
|
||||
## 常用应用
|
||||
|
||||
`HomeCommonMenuStore` 使用 UserDefaults 按角色保存常用应用 URI(key:`home.common.menu.uris.role.{roleCode}`):
|
||||
`HomeCommonMenuStore` 使用 UserDefaults 按账号和角色保存常用应用 URI:
|
||||
- 当前版本使用账号 + 角色作用域保存常用应用 URI,避免不同业务账号的同一角色互相覆盖;旧版角色 key 会在首次读取时迁移。
|
||||
- 首次无配置时,按 Android 规则从当前角色顶层权限 URI 生成默认值:权限多于 4 个取前 4 个,否则全部使用。
|
||||
- 首页常用应用按 Android `Constants.menuList` 白名单过滤;iOS 已接入但 Android 首页未登记的 URI 不进入首页常用应用。
|
||||
- 读取时按当前角色权限过滤不可用 URI。
|
||||
- 添加和移除时按同义 URI 去重。
|
||||
- 不同角色的常用应用互不影响。
|
||||
|
||||
@ -72,6 +72,7 @@ enum HomeRoute: Hashable {
|
||||
enum HomeMenuResolvedRoute: Equatable {
|
||||
case tab(AppTab)
|
||||
case orders(OrdersEntry)
|
||||
case orderPush(OrdersRoute)
|
||||
case destination(HomeRoute)
|
||||
case unsupported(uri: String, title: String, reason: String)
|
||||
case placeholder(uri: String, title: String)
|
||||
|
||||
@ -73,7 +73,7 @@ enum HomeMenuRouter {
|
||||
case "photographer_orders", "/scenic-order-manage":
|
||||
return .orders(.storeOrders)
|
||||
case "verification_order":
|
||||
return .orders(.verificationOrders)
|
||||
return .orderPush(.writeOffList)
|
||||
case "photographer_stats":
|
||||
return .tab(.statistics)
|
||||
case "space_settings", "basic_info":
|
||||
@ -330,7 +330,7 @@ enum HomePermissionRouteAuditor {
|
||||
return HomePermissionRouteAudit(
|
||||
routable: entries.filter { entry in
|
||||
switch entry.route {
|
||||
case .tab, .orders, .destination:
|
||||
case .tab, .orders, .orderPush, .destination:
|
||||
return true
|
||||
case .unsupported, .placeholder:
|
||||
return false
|
||||
|
||||
@ -10,6 +10,37 @@ import Foundation
|
||||
/// 首页常用应用存储服务,负责按当前角色权限过滤和持久化常用 URI。
|
||||
struct HomeCommonMenuStore {
|
||||
static let storageKeyPrefix = "home.common.menu.uris.role."
|
||||
static let accountScopedStorageKeyPrefix = "home.common.menu.uris.account."
|
||||
|
||||
/// 与 Android `Constants.menuList` 对齐的首页菜单 URI 白名单。
|
||||
static let androidHomeMenuURIs: Set<String> = [
|
||||
"space_settings",
|
||||
"wallet",
|
||||
"cloud_management",
|
||||
"asset_management",
|
||||
"task_management",
|
||||
"schedule_management",
|
||||
"system_settings",
|
||||
"message_center",
|
||||
"checkin_points",
|
||||
"sample_management",
|
||||
"live_stream_management",
|
||||
"verification_order",
|
||||
"live_album",
|
||||
"pm",
|
||||
"location_report",
|
||||
"registration_invitation",
|
||||
"store",
|
||||
"fly",
|
||||
"pilot_cert",
|
||||
"task_management_editor",
|
||||
"pm_manager",
|
||||
"pilot_controller",
|
||||
"/scenic-queue",
|
||||
"operating-area",
|
||||
"/scenic-order-manage",
|
||||
"cooperation_order"
|
||||
]
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
@ -27,7 +58,7 @@ struct HomeCommonMenuStore {
|
||||
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let canonical = HomeMenuRouter.canonicalURI(for: trimmed, availableURIs: availableURIs)
|
||||
return availableURIs.contains(canonical) ? canonical : nil
|
||||
return Self.isAndroidHomeMenuURI(canonical) && availableURIs.contains(canonical) ? canonical : nil
|
||||
}
|
||||
let deduplicated = unique(resolved)
|
||||
if deduplicated.count > 4 {
|
||||
@ -40,23 +71,24 @@ struct HomeCommonMenuStore {
|
||||
func load(
|
||||
menuItems: [HomeMenuItem],
|
||||
roleCode: String?,
|
||||
accountScope: String? = nil,
|
||||
legacyRoleId: Int? = nil,
|
||||
topLevelPermissionURIs: [String]
|
||||
) -> [String] {
|
||||
guard let roleCode, !roleCode.isEmpty else { return [] }
|
||||
migrateLegacyStorageIfNeeded(roleCode: roleCode, legacyRoleId: legacyRoleId)
|
||||
migrateLegacyStorageIfNeeded(roleCode: roleCode, accountScope: accountScope, legacyRoleId: legacyRoleId)
|
||||
|
||||
let availableURIs = Set(menuItems.map(\.uri))
|
||||
let key = storageKey(for: roleCode)
|
||||
let key = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
|
||||
let saved = defaults.stringArray(forKey: key)
|
||||
|
||||
if let saved, !saved.isEmpty {
|
||||
let filtered = Self.unique(saved.compactMap { uri -> String? in
|
||||
let resolved = HomeMenuRouter.canonicalURI(for: uri, availableURIs: availableURIs)
|
||||
return availableURIs.contains(resolved) ? resolved : nil
|
||||
return Self.isAndroidHomeMenuURI(resolved) && availableURIs.contains(resolved) ? resolved : nil
|
||||
})
|
||||
if !filtered.isEmpty {
|
||||
save(filtered, roleCode: roleCode)
|
||||
save(filtered, roleCode: roleCode, accountScope: accountScope)
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
@ -65,54 +97,63 @@ struct HomeCommonMenuStore {
|
||||
orderedTopLevelURIs: topLevelPermissionURIs,
|
||||
availableURIs: availableURIs
|
||||
)
|
||||
save(defaultURIs, roleCode: roleCode)
|
||||
save(defaultURIs, roleCode: roleCode, accountScope: accountScope)
|
||||
return defaultURIs
|
||||
}
|
||||
|
||||
/// 将指定 URI 加入常用应用,按别名避免重复添加。
|
||||
func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], roleCode: String?) -> [String] {
|
||||
func add(_ uri: String, current: [String], menuItems: [HomeMenuItem], 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 availableURIs.contains(resolved) else { return current }
|
||||
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]
|
||||
save(next, roleCode: roleCode)
|
||||
save(next, roleCode: roleCode, accountScope: accountScope)
|
||||
return next
|
||||
}
|
||||
|
||||
/// 从常用应用中移除指定 URI,同义 URI 会一起移除。
|
||||
func remove(_ uri: String, current: [String], roleCode: String?) -> [String] {
|
||||
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 }
|
||||
save(next, roleCode: roleCode)
|
||||
save(next, roleCode: roleCode, accountScope: accountScope)
|
||||
return next
|
||||
}
|
||||
|
||||
/// 保存指定角色的常用 URI。
|
||||
func save(_ uris: [String], roleCode: String) {
|
||||
defaults.set(Self.unique(uris), forKey: storageKey(for: roleCode))
|
||||
func save(_ uris: [String], roleCode: String, accountScope: String? = nil) {
|
||||
defaults.set(Self.unique(uris), forKey: Self.storageKey(roleCode: roleCode, accountScope: accountScope))
|
||||
}
|
||||
|
||||
/// 若新 key 无数据,则从 legacy role id key 一次性迁移常用菜单。
|
||||
func migrateLegacyStorageIfNeeded(roleCode: String, legacyRoleId: Int?) {
|
||||
let newKey = storageKey(for: roleCode)
|
||||
guard defaults.stringArray(forKey: newKey) == nil,
|
||||
let legacyRoleId,
|
||||
let legacyData = defaults.stringArray(forKey: legacyStorageKey(for: legacyRoleId)),
|
||||
!legacyData.isEmpty else {
|
||||
/// 若账号级 key 无数据,则从旧 roleCode 或 legacy role id key 一次性迁移常用菜单。
|
||||
func migrateLegacyStorageIfNeeded(roleCode: String, accountScope: String? = nil, legacyRoleId: Int?) {
|
||||
let newKey = Self.storageKey(roleCode: roleCode, accountScope: accountScope)
|
||||
guard defaults.stringArray(forKey: newKey) == nil else { return }
|
||||
|
||||
let legacyKeys = [
|
||||
legacyStorageKey(for: roleCode),
|
||||
legacyRoleId.map { legacyStorageKey(for: $0) }
|
||||
].compactMap { $0 }
|
||||
|
||||
for legacyKey in legacyKeys {
|
||||
guard let legacyData = defaults.stringArray(forKey: legacyKey), !legacyData.isEmpty else { continue }
|
||||
defaults.set(legacyData, forKey: newKey)
|
||||
defaults.removeObject(forKey: legacyKey)
|
||||
return
|
||||
}
|
||||
defaults.set(legacyData, forKey: newKey)
|
||||
defaults.removeObject(forKey: legacyStorageKey(for: legacyRoleId))
|
||||
}
|
||||
|
||||
/// 生成按 role_code 隔离的 UserDefaults key。
|
||||
private func storageKey(for roleCode: String) -> String {
|
||||
Self.storageKeyPrefix + roleCode
|
||||
/// 生成按账号和 role_code 隔离的 UserDefaults key。
|
||||
static func storageKey(roleCode: String, accountScope: String?) -> String {
|
||||
let normalizedScope = accountScope?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if normalizedScope.isEmpty {
|
||||
return storageKeyPrefix + roleCode
|
||||
}
|
||||
return accountScopedStorageKeyPrefix + normalizedScope + ".role." + roleCode
|
||||
}
|
||||
|
||||
/// 旧版按 numeric role id 存储的 key。
|
||||
@ -120,6 +161,16 @@ struct HomeCommonMenuStore {
|
||||
Self.storageKeyPrefix + String(roleId)
|
||||
}
|
||||
|
||||
/// 旧版按 role_code 存储的 key。
|
||||
private func legacyStorageKey(for roleCode: String) -> String {
|
||||
Self.storageKeyPrefix + roleCode
|
||||
}
|
||||
|
||||
/// 判断 URI 是否属于 Android 首页菜单白名单。
|
||||
private static func isAndroidHomeMenuURI(_ uri: String) -> Bool {
|
||||
androidHomeMenuURIs.contains(uri)
|
||||
}
|
||||
|
||||
/// 按 URI 别名去重,保留首次出现的 URI。
|
||||
private static func unique(_ uris: [String]) -> [String] {
|
||||
var seen = Set<String>()
|
||||
|
||||
@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
/// 首页全部功能视图,展示常用应用和当前角色拥有的更多功能。
|
||||
struct HomeMoreFunctionsView: View {
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@EnvironmentObject private var permissionContext: PermissionContext
|
||||
@EnvironmentObject private var appRouter: AppRouter
|
||||
@EnvironmentObject private var router: RouterPath
|
||||
@ -124,13 +125,19 @@ struct HomeMoreFunctionsView: View {
|
||||
Button {
|
||||
let roleCode = permissionContext.currentAppRole?.rawValue
|
||||
if isCommon {
|
||||
commonUris = commonMenuStore.remove(item.uri, current: commonUris, roleCode: roleCode)
|
||||
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
|
||||
roleCode: roleCode,
|
||||
accountScope: accountContext.accountCachePrefix
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
@ -187,6 +194,9 @@ struct HomeMoreFunctionsView: View {
|
||||
appRouter.select(tab)
|
||||
case .orders(let entry):
|
||||
appRouter.selectOrders(entry: entry)
|
||||
case .orderPush(let route):
|
||||
appRouter.select(.orders)
|
||||
appRouter.router(for: .orders).navigate(to: .orders(route))
|
||||
case .destination(let route):
|
||||
if route == .moreFunctions {
|
||||
return
|
||||
@ -210,6 +220,7 @@ struct HomeMoreFunctionsView: View {
|
||||
commonUris = commonMenuStore.load(
|
||||
menuItems: viewModel.menuItems,
|
||||
roleCode: roleCode,
|
||||
accountScope: accountContext.accountCachePrefix,
|
||||
legacyRoleId: legacyRoleId,
|
||||
topLevelPermissionURIs: permissionContext.topLevelPermissionURIs(for: roleCode)
|
||||
)
|
||||
|
||||
@ -395,6 +395,9 @@ struct HomeView: View {
|
||||
appRouter.select(tab)
|
||||
case .orders(let entry):
|
||||
appRouter.selectOrders(entry: entry)
|
||||
case .orderPush(let route):
|
||||
appRouter.select(.orders)
|
||||
appRouter.router(for: .orders).navigate(to: .orders(route))
|
||||
case .destination(let route):
|
||||
router.navigate(to: .home(route))
|
||||
case .unsupported(let uri, let title, _):
|
||||
@ -415,6 +418,7 @@ struct HomeView: View {
|
||||
commonUris = commonMenuStore.load(
|
||||
menuItems: viewModel.menuItems,
|
||||
roleCode: roleCode,
|
||||
accountScope: accountContext.accountCachePrefix,
|
||||
legacyRoleId: legacyRoleId,
|
||||
topLevelPermissionURIs: permissionContext.topLevelPermissionURIs(for: roleCode)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user