新增门店订单与核销管理列表,对齐 Android 订单能力并完善账号级缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 14:46:10 +08:00
parent c4de3d17d0
commit 08492df6ce
51 changed files with 3668 additions and 102 deletions

View File

@ -18,8 +18,10 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
## 常用应用
`HomeCommonMenuStore` 使用 UserDefaults 按角色保存常用应用 URIkey`home.common.menu.uris.role.{roleCode}`
`HomeCommonMenuStore` 使用 UserDefaults 按账号和角色保存常用应用 URI
- 当前版本使用账号 + 角色作用域保存常用应用 URI避免不同业务账号的同一角色互相覆盖旧版角色 key 会在首次读取时迁移。
- 首次无配置时,按 Android 规则从当前角色顶层权限 URI 生成默认值:权限多于 4 个取前 4 个,否则全部使用。
- 首页常用应用按 Android `Constants.menuList` 白名单过滤iOS 已接入但 Android 首页未登记的 URI 不进入首页常用应用。
- 读取时按当前角色权限过滤不可用 URI。
- 添加和移除时按同义 URI 去重。
- 不同角色的常用应用互不影响。

View File

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

View File

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

View File

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

View File

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

View File

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