修复账号切换后「我的」页当前账号展示错误,并统一举报摄影师首页 URI。
切换账号时以用户选中账号为准写入展示标题与业务作用域,避免沿用旧门店名称;同时将首页举报摄影师入口 URI 对齐为 report_photographer。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -63,11 +63,22 @@ struct BusinessScope: Codable, Equatable, Identifiable {
|
||||
final class AccountContext: ObservableObject {
|
||||
@Published private(set) var profile: AccountProfile?
|
||||
@Published private(set) var accountType: String?
|
||||
@Published private(set) var businessUserId: Int?
|
||||
/// 当前账号在「我的」页面展示的标题,来源于登录/切换时选中的账号名称。
|
||||
@Published private(set) var currentAccountDisplayTitle: String?
|
||||
@Published private(set) var scenicScopes: [BusinessScope] = []
|
||||
@Published private(set) var storeScopes: [BusinessScope] = []
|
||||
@Published var currentScenic: BusinessScope?
|
||||
@Published var currentStore: BusinessScope?
|
||||
|
||||
/// 当前账号标识,与 AccountSwitchAccount.id 规则一致。
|
||||
var currentAccountIdentifier: String? {
|
||||
guard let businessUserId, businessUserId > 0 else { return nil }
|
||||
let type = accountType?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !type.isEmpty else { return nil }
|
||||
return "\(type)_\(businessUserId)"
|
||||
}
|
||||
|
||||
/// 当前账号缓存前缀,与 Android 的账号级缓存作用域保持一致。
|
||||
var accountCachePrefix: String? {
|
||||
let userId = profile?.userId.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
@ -77,9 +88,20 @@ final class AccountContext: ObservableObject {
|
||||
}
|
||||
|
||||
/// 应用登录成功后写入账号资料。
|
||||
func applyLogin(profile: AccountProfile? = nil, accountType: String? = nil) {
|
||||
func applyLogin(
|
||||
profile: AccountProfile? = nil,
|
||||
accountType: String? = nil,
|
||||
businessUserId: Int? = nil,
|
||||
displayTitle: String? = nil
|
||||
) {
|
||||
self.profile = profile
|
||||
self.accountType = accountType?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let businessUserId, businessUserId > 0 {
|
||||
self.businessUserId = businessUserId
|
||||
}
|
||||
if let displayTitle = normalizedText(displayTitle) {
|
||||
currentAccountDisplayTitle = displayTitle
|
||||
}
|
||||
}
|
||||
|
||||
/// 替换当前账号资料,通常用于用户信息刷新后同步全局展示。
|
||||
@ -92,10 +114,29 @@ final class AccountContext: ObservableObject {
|
||||
scenic: [BusinessScope],
|
||||
stores: [BusinessScope],
|
||||
currentScenicId: Int? = nil,
|
||||
currentStoreId: Int? = nil
|
||||
currentStoreId: Int? = nil,
|
||||
explicitSelection: Bool = false
|
||||
) {
|
||||
scenicScopes = scenic
|
||||
storeScopes = stores
|
||||
|
||||
if explicitSelection {
|
||||
currentScenic = currentScenicId.flatMap { id in
|
||||
scenic.first { $0.id == id }
|
||||
} ?? scenic.first
|
||||
if let storeId = currentStoreId, storeId > 0 {
|
||||
currentStore = resolvedStore(
|
||||
in: stores,
|
||||
currentScenic: currentScenic,
|
||||
preferredStoreId: storeId,
|
||||
allowFallbackToFirst: false
|
||||
)
|
||||
} else {
|
||||
currentStore = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
currentScenic = currentScenicId.flatMap { id in
|
||||
scenic.first { $0.id == id }
|
||||
} ?? currentScenic.flatMap { current in
|
||||
@ -104,7 +145,8 @@ final class AccountContext: ObservableObject {
|
||||
currentStore = resolvedStore(
|
||||
in: stores,
|
||||
currentScenic: currentScenic,
|
||||
preferredStoreId: currentStoreId ?? currentStore?.id
|
||||
preferredStoreId: currentStoreId ?? currentStore?.id,
|
||||
allowFallbackToFirst: true
|
||||
)
|
||||
}
|
||||
|
||||
@ -115,7 +157,8 @@ final class AccountContext: ObservableObject {
|
||||
currentStore = resolvedStore(
|
||||
in: storeScopes,
|
||||
currentScenic: scenic,
|
||||
preferredStoreId: currentStore?.id
|
||||
preferredStoreId: currentStore?.id,
|
||||
allowFallbackToFirst: true
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,6 +177,8 @@ final class AccountContext: ObservableObject {
|
||||
func reset() {
|
||||
profile = nil
|
||||
accountType = nil
|
||||
businessUserId = nil
|
||||
currentAccountDisplayTitle = nil
|
||||
scenicScopes = []
|
||||
storeScopes = []
|
||||
currentScenic = nil
|
||||
@ -144,7 +189,8 @@ final class AccountContext: ObservableObject {
|
||||
private func resolvedStore(
|
||||
in stores: [BusinessScope],
|
||||
currentScenic: BusinessScope?,
|
||||
preferredStoreId: Int?
|
||||
preferredStoreId: Int?,
|
||||
allowFallbackToFirst: Bool
|
||||
) -> BusinessScope? {
|
||||
let scenicId = currentScenic?.id
|
||||
let storesForScenic = stores.filter { store in
|
||||
@ -155,6 +201,13 @@ final class AccountContext: ObservableObject {
|
||||
let store = storesForScenic.first(where: { $0.id == preferredStoreId }) {
|
||||
return store
|
||||
}
|
||||
guard allowFallbackToFirst else { return nil }
|
||||
return storesForScenic.first
|
||||
}
|
||||
|
||||
/// 返回去空白后的非空文本。
|
||||
private func normalizedText(_ value: String?) -> String? {
|
||||
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: String? = nil,
|
||||
cachedLegacyRoleId: Int? = nil,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
try await restorePermissions(
|
||||
permissionContext: permissionContext,
|
||||
@ -35,7 +36,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: cachedCurrentRoleCode,
|
||||
cachedLegacyRoleId: cachedLegacyRoleId,
|
||||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||||
cachedCurrentStoreId: cachedCurrentStoreId
|
||||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
)
|
||||
try await refreshSupplementalContext(
|
||||
accountContext: accountContext,
|
||||
@ -43,7 +45,8 @@ struct AccountContextLoader {
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI,
|
||||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||||
cachedCurrentStoreId: cachedCurrentStoreId
|
||||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,7 +58,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: String? = nil,
|
||||
cachedLegacyRoleId: Int? = nil,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
let rolePermissions = try await accountContextAPI.rolePermissions()
|
||||
permissionContext.replaceRolePermissions(
|
||||
@ -70,7 +74,8 @@ struct AccountContextLoader {
|
||||
scenic: roleScenicScopes,
|
||||
stores: accountContext.storeScopes,
|
||||
currentScenicId: cachedCurrentScenicId,
|
||||
currentStoreId: cachedCurrentStoreId
|
||||
currentStoreId: cachedCurrentStoreId,
|
||||
explicitSelection: explicitScopeSelection
|
||||
)
|
||||
}
|
||||
|
||||
@ -81,7 +86,8 @@ struct AccountContextLoader {
|
||||
profileAPI: UserProfileServing,
|
||||
accountContextAPI: AccountContextServing,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
let userInfo = try await profileAPI.userInfo()
|
||||
let scenicResponse = await loadScenicList(
|
||||
@ -94,19 +100,57 @@ struct AccountContextLoader {
|
||||
permissionContext: permissionContext,
|
||||
scenicResponse: scenicResponse
|
||||
)
|
||||
let storeScopes = storesResponse?.list.map { store in
|
||||
let fetchedStoreScopes = storesResponse?.list.map { store in
|
||||
BusinessScope(id: store.id, name: store.name, kind: .store, parentScenicId: store.scenicId)
|
||||
} ?? []
|
||||
// 门店列表接口失败或为空时,保留登录/切换时已写入的门店作用域,避免门店账号展示回退成景区名。
|
||||
let storeScopes = fetchedStoreScopes.isEmpty ? accountContext.storeScopes : fetchedStoreScopes
|
||||
|
||||
accountContext.replaceProfile(profile)
|
||||
accountContext.replaceScopes(
|
||||
scenic: scenicScopes,
|
||||
stores: storeScopes,
|
||||
currentScenicId: cachedCurrentScenicId ?? accountContext.currentScenic?.id,
|
||||
currentStoreId: cachedCurrentStoreId ?? accountContext.currentStore?.id
|
||||
currentScenicId: resolvedScenicId(
|
||||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||||
accountContext: accountContext,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
),
|
||||
currentStoreId: resolvedStoreId(
|
||||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||||
accountContext: accountContext,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
),
|
||||
explicitSelection: explicitScopeSelection
|
||||
)
|
||||
}
|
||||
|
||||
/// 解析刷新时应使用的景区 ID。
|
||||
private func resolvedScenicId(
|
||||
cachedCurrentScenicId: Int?,
|
||||
accountContext: AccountContext,
|
||||
explicitScopeSelection: Bool
|
||||
) -> Int? {
|
||||
if explicitScopeSelection {
|
||||
return cachedCurrentScenicId ?? accountContext.currentScenic?.id
|
||||
}
|
||||
return cachedCurrentScenicId ?? accountContext.currentScenic?.id
|
||||
}
|
||||
|
||||
/// 解析刷新时应使用的门店 ID;景区账号在显式选择模式下必须清空门店。
|
||||
private func resolvedStoreId(
|
||||
cachedCurrentStoreId: Int?,
|
||||
accountContext: AccountContext,
|
||||
explicitScopeSelection: Bool
|
||||
) -> Int? {
|
||||
if explicitScopeSelection {
|
||||
if accountContext.accountType == V9ScenicUser.accountTypeValue {
|
||||
return nil
|
||||
}
|
||||
return cachedCurrentStoreId
|
||||
}
|
||||
return cachedCurrentStoreId ?? accountContext.currentStore?.id
|
||||
}
|
||||
|
||||
/// 景区列表接口失败时,从角色权限中的景区数据去重构造兜底列表。
|
||||
private func loadScenicList(
|
||||
accountContextAPI: AccountContextServing,
|
||||
|
||||
@ -52,7 +52,8 @@ final class AuthSessionCoordinator {
|
||||
accountContext: AccountContext,
|
||||
permissionContext: PermissionContext,
|
||||
profileAPI: UserProfileServing,
|
||||
accountContextAPI: AccountContextServing
|
||||
accountContextAPI: AccountContextServing,
|
||||
selectedAccount: AccountSwitchAccount? = nil
|
||||
) async throws {
|
||||
let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try tokenStore.save(token)
|
||||
@ -62,9 +63,22 @@ final class AuthSessionCoordinator {
|
||||
let profile = response.primaryProfile
|
||||
let scenicScopes = response.scenicScopes
|
||||
let storeScopes = response.storeScopes
|
||||
let identity = response.currentAccountIdentity
|
||||
accountContext.applyLogin(profile: profile, accountType: identity?.accountType)
|
||||
accountContext.replaceScopes(scenic: scenicScopes, stores: storeScopes)
|
||||
let identity = resolvedIdentity(from: response, selectedAccount: selectedAccount)
|
||||
let scopeSelection = selectedAccount?.preferredScopeSelection ?? response.preferredScopeSelection
|
||||
let displayTitle = nonEmpty(selectedAccount?.title) ?? response.currentAccountDisplayTitle
|
||||
accountContext.applyLogin(
|
||||
profile: profile,
|
||||
accountType: identity?.accountType,
|
||||
businessUserId: identity?.businessUserId,
|
||||
displayTitle: displayTitle
|
||||
)
|
||||
accountContext.replaceScopes(
|
||||
scenic: scenicScopes,
|
||||
stores: storeScopes,
|
||||
currentScenicId: scopeSelection.scenicId,
|
||||
currentStoreId: scopeSelection.storeId,
|
||||
explicitSelection: true
|
||||
)
|
||||
appSession.beginRestoring(token: token)
|
||||
|
||||
do {
|
||||
@ -74,8 +88,9 @@ final class AuthSessionCoordinator {
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI,
|
||||
cachedCurrentRoleCode: response.currentAccountRoleCode,
|
||||
cachedCurrentScenicId: accountContext.currentScenic?.id,
|
||||
cachedCurrentStoreId: accountContext.currentStore?.id
|
||||
cachedCurrentScenicId: scopeSelection.scenicId,
|
||||
cachedCurrentStoreId: scopeSelection.storeId,
|
||||
explicitScopeSelection: true
|
||||
)
|
||||
} catch {
|
||||
if APIError.isAuthenticationExpired(error) {
|
||||
@ -153,6 +168,7 @@ final class AuthSessionCoordinator {
|
||||
profile: profile,
|
||||
accountType: accountType,
|
||||
businessUserId: businessUserId,
|
||||
currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle,
|
||||
currentRoleCode: permissionContext?.currentAppRole?.rawValue ?? currentRoleCode,
|
||||
scenicScopes: accountContext.scenicScopes,
|
||||
storeScopes: accountContext.storeScopes,
|
||||
@ -162,6 +178,17 @@ final class AuthSessionCoordinator {
|
||||
)
|
||||
}
|
||||
|
||||
/// 优先使用用户明确选择的账号身份,避免 set-user 响应仍携带旧 is_current 时解析错误。
|
||||
private func resolvedIdentity(
|
||||
from response: V9AuthResponse,
|
||||
selectedAccount: AccountSwitchAccount?
|
||||
) -> (accountType: String, businessUserId: Int)? {
|
||||
if let selectedAccount, selectedAccount.businessUserId > 0 {
|
||||
return (selectedAccount.accountType, selectedAccount.businessUserId)
|
||||
}
|
||||
return response.currentAccountIdentity.map { ($0.accountType, $0.businessUserId) }
|
||||
}
|
||||
|
||||
/// 将用户资料接口响应转换为全局账号资料。
|
||||
private func makeProfile(from userInfo: UserInfoResponse, fallback: AccountProfile?) -> AccountProfile {
|
||||
AccountProfile(
|
||||
@ -185,27 +212,3 @@ struct LoginPreferences: Equatable {
|
||||
let privacyAgreementAccepted: Bool
|
||||
}
|
||||
|
||||
private extension V9AuthResponse {
|
||||
/// 当前账号身份实体,表示登录后实际选中的业务账号。
|
||||
struct CurrentAccountIdentity {
|
||||
let accountType: String
|
||||
let businessUserId: Int
|
||||
}
|
||||
|
||||
/// 提取当前账号身份,优先使用后端标记的 isCurrent 账号。
|
||||
var currentAccountIdentity: CurrentAccountIdentity? {
|
||||
if let storeUser = storeUsers.first(where: \.isCurrent) ?? storeUsers.first {
|
||||
return CurrentAccountIdentity(
|
||||
accountType: storeUser.accountType,
|
||||
businessUserId: storeUser.businessUserId
|
||||
)
|
||||
}
|
||||
if let scenicUser = scenicUsers.first(where: \.isCurrent) ?? scenicUsers.first {
|
||||
return CurrentAccountIdentity(
|
||||
accountType: scenicUser.accountType,
|
||||
businessUserId: scenicUser.businessUserId
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +89,18 @@ final class SessionBootstrapper {
|
||||
/// 将缓存快照恢复到账号上下文。
|
||||
private func restoreCachedSnapshot(to accountContext: AccountContext) -> AccountSnapshot? {
|
||||
guard let snapshot = snapshotStore.load() else { return nil }
|
||||
accountContext.applyLogin(profile: snapshot.profile, accountType: snapshot.accountType)
|
||||
accountContext.applyLogin(
|
||||
profile: snapshot.profile,
|
||||
accountType: snapshot.accountType,
|
||||
businessUserId: snapshot.businessUserId,
|
||||
displayTitle: snapshot.currentAccountDisplayTitle
|
||||
)
|
||||
accountContext.replaceScopes(
|
||||
scenic: snapshot.scenicScopes,
|
||||
stores: snapshot.storeScopes,
|
||||
currentScenicId: snapshot.currentScenicId,
|
||||
currentStoreId: snapshot.currentStoreId
|
||||
currentStoreId: snapshot.currentStoreId,
|
||||
explicitSelection: true
|
||||
)
|
||||
return snapshot
|
||||
}
|
||||
@ -198,6 +204,7 @@ final class SessionBootstrapper {
|
||||
profile: accountContext.profile,
|
||||
accountType: accountContext.accountType ?? existing?.accountType,
|
||||
businessUserId: existing?.businessUserId,
|
||||
currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle ?? existing?.currentAccountDisplayTitle,
|
||||
currentRoleCode: permissionContext.currentAppRole?.rawValue ?? existing?.currentRoleCode,
|
||||
scenicScopes: accountContext.scenicScopes,
|
||||
storeScopes: accountContext.storeScopes,
|
||||
|
||||
@ -13,6 +13,7 @@ struct AccountSnapshot: Codable, Equatable {
|
||||
var profile: AccountProfile?
|
||||
var accountType: String?
|
||||
var businessUserId: Int?
|
||||
var currentAccountDisplayTitle: String?
|
||||
var currentRoleCode: String?
|
||||
/// 旧版快照字段,仅用于解码与一次性迁移。
|
||||
var currentRoleId: Int?
|
||||
@ -26,6 +27,7 @@ struct AccountSnapshot: Codable, Equatable {
|
||||
profile: AccountProfile? = nil,
|
||||
accountType: String? = nil,
|
||||
businessUserId: Int? = nil,
|
||||
currentAccountDisplayTitle: String? = nil,
|
||||
currentRoleCode: String? = nil,
|
||||
currentRoleId: Int? = nil,
|
||||
scenicScopes: [BusinessScope] = [],
|
||||
@ -36,6 +38,7 @@ struct AccountSnapshot: Codable, Equatable {
|
||||
self.profile = profile
|
||||
self.accountType = accountType
|
||||
self.businessUserId = businessUserId
|
||||
self.currentAccountDisplayTitle = currentAccountDisplayTitle
|
||||
self.currentRoleCode = currentRoleCode
|
||||
self.currentRoleId = currentRoleId
|
||||
self.scenicScopes = scenicScopes
|
||||
@ -117,6 +120,7 @@ final class AccountSnapshotStore {
|
||||
profile: accountContext.profile ?? existing?.profile,
|
||||
accountType: accountContext.accountType ?? existing?.accountType,
|
||||
businessUserId: existing?.businessUserId,
|
||||
currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle ?? existing?.currentAccountDisplayTitle,
|
||||
currentRoleCode: currentRoleCode ?? existing?.currentRoleCode,
|
||||
scenicScopes: accountContext.scenicScopes,
|
||||
storeScopes: accountContext.storeScopes,
|
||||
|
||||
@ -76,6 +76,14 @@ struct AccountSwitchAccount: Identifiable, Hashable {
|
||||
}
|
||||
return SetUserRequest(ssUserId: businessUserId)
|
||||
}
|
||||
|
||||
/// 当前账号对应的景区和门店 ID,用于切换后校准业务作用域。
|
||||
var preferredScopeSelection: (scenicId: Int?, storeId: Int?) {
|
||||
if isStoreUser {
|
||||
return (scenicId, storeId)
|
||||
}
|
||||
return (scenicId, nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录账号选择载荷,保存临时 token 和待用户选择的账号列表。
|
||||
@ -100,9 +108,9 @@ struct V9AuthResponse: Decodable, Equatable {
|
||||
scenicUsers.map { $0.toAccountSwitchAccount() } + storeUsers.map { $0.toAccountSwitchAccount() }
|
||||
}
|
||||
|
||||
/// 提取当前或首个账号作为全局账号资料。
|
||||
/// 提取当前或首个账号作为全局账号资料,优先使用 isCurrent 标记的账号。
|
||||
var primaryProfile: AccountProfile? {
|
||||
if let storeUser = storeUsers.first(where: \.isCurrent) ?? storeUsers.first {
|
||||
if let storeUser = resolvedCurrentStoreUser {
|
||||
return AccountProfile(
|
||||
userId: String(storeUser.userId),
|
||||
displayName: storeUser.displayName,
|
||||
@ -111,7 +119,7 @@ struct V9AuthResponse: Decodable, Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
if let scenicUser = scenicUsers.first(where: \.isCurrent) ?? scenicUsers.first {
|
||||
if let scenicUser = resolvedCurrentScenicUser {
|
||||
return AccountProfile(
|
||||
userId: String(scenicUser.userId),
|
||||
displayName: scenicUser.displayName,
|
||||
@ -123,6 +131,60 @@ struct V9AuthResponse: Decodable, Equatable {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 当前生效账号的类型和业务 ID,用于登录/切换后写入账号上下文。
|
||||
var currentAccountIdentity: (accountType: String, businessUserId: Int)? {
|
||||
if let storeUser = resolvedCurrentStoreUser {
|
||||
return (storeUser.accountType, storeUser.businessUserId)
|
||||
}
|
||||
if let scenicUser = resolvedCurrentScenicUser {
|
||||
return (scenicUser.accountType, scenicUser.businessUserId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 当前生效账号对应的景区和门店 ID,用于登录/切换后校准业务作用域。
|
||||
var preferredScopeSelection: (scenicId: Int?, storeId: Int?) {
|
||||
if let storeUser = resolvedCurrentStoreUser {
|
||||
return (
|
||||
storeUser.scenicId > 0 ? storeUser.scenicId : nil,
|
||||
storeUser.storeId > 0 ? storeUser.storeId : nil
|
||||
)
|
||||
}
|
||||
if let scenicUser = resolvedCurrentScenicUser {
|
||||
return (scenicUser.scenicId > 0 ? scenicUser.scenicId : nil, nil)
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
/// 当前生效账号在「我的」页面展示的标题。
|
||||
var currentAccountDisplayTitle: String? {
|
||||
if let storeUser = resolvedCurrentStoreUser {
|
||||
return storeUser.displayName.nonEmpty
|
||||
}
|
||||
if let scenicUser = resolvedCurrentScenicUser {
|
||||
return scenicUser.displayName.nonEmpty
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 解析当前门店账号:isCurrent 优先,且在有景区 isCurrent 时不回退到其他门店。
|
||||
private var resolvedCurrentStoreUser: V9StoreUser? {
|
||||
if let current = storeUsers.first(where: \.isCurrent) {
|
||||
return current
|
||||
}
|
||||
guard !scenicUsers.contains(where: \.isCurrent) else { return nil }
|
||||
return storeUsers.first
|
||||
}
|
||||
|
||||
/// 解析当前景区账号:isCurrent 优先,且在有门店 isCurrent 时不回退到其他景区。
|
||||
private var resolvedCurrentScenicUser: V9ScenicUser? {
|
||||
if let current = scenicUsers.first(where: \.isCurrent) {
|
||||
return current
|
||||
}
|
||||
guard !storeUsers.contains(where: \.isCurrent) else { return nil }
|
||||
return scenicUsers.first
|
||||
}
|
||||
|
||||
/// 提取登录响应中的景区作用域,并按当前账号优先排序。
|
||||
var scenicScopes: [BusinessScope] {
|
||||
var seen = Set<Int>()
|
||||
@ -159,10 +221,10 @@ struct V9AuthResponse: Decodable, Equatable {
|
||||
|
||||
/// 当前账号的 app_role_code,用于登录后初始角色匹配。
|
||||
var currentAccountRoleCode: String? {
|
||||
if let storeUser = storeUsers.first(where: \.isCurrent) ?? storeUsers.first {
|
||||
if let storeUser = resolvedCurrentStoreUser {
|
||||
return nonEmpty(storeUser.resolvedAppRoleCode)
|
||||
}
|
||||
if let scenicUser = scenicUsers.first(where: \.isCurrent) ?? scenicUsers.first {
|
||||
if let scenicUser = resolvedCurrentScenicUser {
|
||||
return nonEmpty(scenicUser.resolvedAppRoleCode)
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -179,7 +179,7 @@ struct LoginView: View {
|
||||
do {
|
||||
try await globalLoading.withLoading(message: "账号切换中...") {
|
||||
let response = try await viewModel.selectAccount(account, authAPI: authAPI)
|
||||
await completeLogin(with: response)
|
||||
await completeLogin(with: response, selectedAccount: account)
|
||||
}
|
||||
} catch is CancellationError {
|
||||
// Keep cancellation silent; the current task was abandoned by SwiftUI.
|
||||
@ -190,7 +190,7 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
/// 将最终登录响应写入全局会话和账号上下文。
|
||||
private func completeLogin(with response: V9AuthResponse) async {
|
||||
private func completeLogin(with response: V9AuthResponse, selectedAccount: AccountSwitchAccount? = nil) async {
|
||||
do {
|
||||
try await authSessionCoordinator.completeLogin(
|
||||
with: response,
|
||||
@ -200,7 +200,8 @@ struct LoginView: View {
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI
|
||||
accountContextAPI: accountContextAPI,
|
||||
selectedAccount: selectedAccount
|
||||
)
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
|
||||
@ -80,7 +80,7 @@ allFunctions = 顶层权限
|
||||
.映射为 HomeMenuItem
|
||||
```
|
||||
|
||||
`androidHomeMenuURIs` 与 Android `Constants.menuList` 登记 URI 一致。已登记且会出现在「全部功能」页的 URI 包括 `location_report`、`photographer_report`(举报摄影师)、`wallet`、`message_center` 等。
|
||||
`androidHomeMenuURIs` 与 Android `Constants.menuList` 登记 URI 一致。已登记且会出现在「全部功能」页的 URI 包括 `location_report`、`report_photographer`(举报摄影师)、`wallet`、`message_center` 等。
|
||||
|
||||
以下典型 URI **不会**出现在「全部功能」页,即使接口返回了顶层权限:
|
||||
|
||||
|
||||
@ -62,4 +62,4 @@ Home 模块负责登录后的首页工作台,包括当前景区展示、工作
|
||||
|
||||
后续迁移具体首页子模块时,应先在 `HomeRoute` 增加目标页面,再更新 `HomeMenuRouter.resolve` 对应 URI 的映射,并同步补充单元测试。
|
||||
|
||||
`photographer_report` 已由 `Features/WildPhotographerReport` 接管。
|
||||
`report_photographer` 已由 `Features/WildPhotographerReport` 接管。
|
||||
|
||||
@ -46,7 +46,7 @@ enum HomeMenuRouter {
|
||||
"pm_manager": "店铺项目管理",
|
||||
"project_edit": "项目管理",
|
||||
"location_report": "位置上报",
|
||||
"photographer_report": "举报摄影师",
|
||||
"report_photographer": "举报摄影师",
|
||||
"registration_invitation": "注册邀请",
|
||||
"store": "店铺管理",
|
||||
"fly": "飞行管理",
|
||||
@ -132,7 +132,7 @@ enum HomeMenuRouter {
|
||||
return .destination(.locationReport)
|
||||
case "location_report_history":
|
||||
return .destination(.locationReportHistory)
|
||||
case "photographer_report":
|
||||
case "report_photographer":
|
||||
return .destination(.photographerReport)
|
||||
case "deposit_order_detail", "deposit_order", "deposit_order_shooting_info":
|
||||
return .destination(.depositOrders)
|
||||
@ -219,7 +219,7 @@ enum HomeMenuRouter {
|
||||
"pm_manager",
|
||||
"project_edit",
|
||||
"location_report",
|
||||
"photographer_report",
|
||||
"report_photographer",
|
||||
"registration_invitation",
|
||||
"photographer_invite",
|
||||
"invite_record",
|
||||
@ -298,7 +298,7 @@ enum HomeMenuRouter {
|
||||
return "注册邀请"
|
||||
case "location_report":
|
||||
return "位置上报"
|
||||
case "photographer_report":
|
||||
case "report_photographer":
|
||||
return "举报摄影师"
|
||||
case "pm", "project_edit":
|
||||
return "项目管理"
|
||||
|
||||
@ -30,7 +30,7 @@ struct HomeCommonMenuStore {
|
||||
"travel_album",
|
||||
"pm",
|
||||
"location_report",
|
||||
"photographer_report",
|
||||
"report_photographer",
|
||||
"registration_invitation",
|
||||
"store",
|
||||
"fly",
|
||||
|
||||
@ -54,7 +54,7 @@ enum HomeIconCatalog {
|
||||
"circle.grid.2x2.fill"
|
||||
case "location_report", "location_report_history":
|
||||
"mappin.circle.fill"
|
||||
case "photographer_report":
|
||||
case "report_photographer":
|
||||
"shield.fill"
|
||||
case "registration_invitation", "photographer_invite":
|
||||
"envelope.open.fill"
|
||||
|
||||
@ -125,3 +125,7 @@ DEBUG 构建下,“我的”列表会额外展示“首页调试”入口,
|
||||
- 无实名认证信息时:点击去实名认证。
|
||||
- 账号切换失败只展示 Toast,不清空登录态。
|
||||
- 实名认证失败只影响实名认证页面,不影响登录态和账号上下文。
|
||||
- 「当前账号」展示规则:
|
||||
- 门店账号(`store_user`)展示门店名称,不回退到景区名称。
|
||||
- 景区账号(`scenic_user`)展示景区名称。
|
||||
- 展示标题在登录/切换成功时写入 `AccountContext.currentAccountDisplayTitle`,切换账号时必须同步更新,不能沿用上一次账号名称。
|
||||
|
||||
@ -212,7 +212,8 @@ struct AccountSwitchView: View {
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI
|
||||
accountContextAPI: accountContextAPI,
|
||||
selectedAccount: account
|
||||
)
|
||||
}
|
||||
appRouter.reset()
|
||||
@ -226,14 +227,7 @@ struct AccountSwitchView: View {
|
||||
}
|
||||
|
||||
private var currentAccountId: String? {
|
||||
guard let current = accountContext.profile else { return nil }
|
||||
if let store = accountContext.currentStore {
|
||||
return "\(V9StoreUser.accountTypeValue)_\(store.id)"
|
||||
}
|
||||
if let scenic = accountContext.currentScenic {
|
||||
return "\(V9ScenicUser.accountTypeValue)_\(scenic.id)"
|
||||
}
|
||||
return current.userId.isEmpty ? nil : current.userId
|
||||
accountContext.currentAccountIdentifier
|
||||
}
|
||||
|
||||
/// 判断账号是否为当前已选账号。
|
||||
|
||||
@ -494,21 +494,52 @@ struct ProfileView: View {
|
||||
}
|
||||
|
||||
private var currentAccountDisplayName: String {
|
||||
nonEmpty(accountContext.currentAccountDisplayTitle)
|
||||
?? (isStoreAccountContext ? resolvedStoreAccountName : resolvedScenicAccountName)
|
||||
}
|
||||
|
||||
/// 门店账号展示名:优先门店名称,不回退到景区名称。
|
||||
private var resolvedStoreAccountName: String {
|
||||
nonEmpty(accountContext.currentStore?.name)
|
||||
?? nonEmpty(accountContext.currentScenic?.name)
|
||||
?? accountContext.profile?.displayName
|
||||
?? nonEmpty(matchedStoreScope?.name)
|
||||
?? nonEmpty(accountContext.profile?.displayName)
|
||||
?? viewModel.displayNickname
|
||||
}
|
||||
|
||||
/// 景区账号展示名:优先景区名称。
|
||||
private var resolvedScenicAccountName: String {
|
||||
nonEmpty(accountContext.currentScenic?.name)
|
||||
?? nonEmpty(accountContext.profile?.displayName)
|
||||
?? viewModel.displayNickname
|
||||
}
|
||||
|
||||
/// 当前上下文是否应按门店账号展示。
|
||||
private var isStoreAccountContext: Bool {
|
||||
accountContext.accountType == V9StoreUser.accountTypeValue
|
||||
}
|
||||
|
||||
/// 从门店作用域中匹配当前门店,用于 currentStore 尚未就绪时的展示兜底。
|
||||
private var matchedStoreScope: BusinessScope? {
|
||||
if let currentStore = accountContext.currentStore {
|
||||
return currentStore
|
||||
}
|
||||
guard accountContext.accountType == V9StoreUser.accountTypeValue else { return nil }
|
||||
if accountContext.storeScopes.count == 1 {
|
||||
return accountContext.storeScopes.first
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var currentAccountTypeText: String {
|
||||
if accountContext.currentStore != nil {
|
||||
switch accountContext.accountType {
|
||||
case V9StoreUser.accountTypeValue:
|
||||
return "门店账号"
|
||||
}
|
||||
if accountContext.currentScenic != nil {
|
||||
case V9ScenicUser.accountTypeValue:
|
||||
return "景区账号"
|
||||
}
|
||||
default:
|
||||
return nonEmpty(viewModel.userInfo?.roleName) ?? "账号"
|
||||
}
|
||||
}
|
||||
|
||||
private var nicknameBinding: Binding<String> {
|
||||
Binding(
|
||||
|
||||
@ -576,6 +576,7 @@ struct ProfileSpaceView: View {
|
||||
profile: profile,
|
||||
accountType: accountContext.accountType ?? existing?.accountType,
|
||||
businessUserId: existing?.businessUserId,
|
||||
currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle ?? existing?.currentAccountDisplayTitle,
|
||||
currentRoleCode: existing?.currentRoleCode,
|
||||
scenicScopes: accountContext.scenicScopes,
|
||||
storeScopes: accountContext.storeScopes,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 模块职责
|
||||
|
||||
WildPhotographerReport 模块负责首页 `photographer_report` 入口,为摄影师和景区运营提供「举报摄影师」能力。
|
||||
WildPhotographerReport 模块负责首页 `report_photographer` 入口,为摄影师和景区运营提供「举报摄影师」能力。
|
||||
|
||||
本模块与 `LocationReport`(位置上报)无关:前者是用户举报疑似打野/未戴工牌摄影师,后者是摄影师 GPS 打卡。
|
||||
|
||||
@ -49,7 +49,7 @@ flowchart TD
|
||||
|
||||
## 路由
|
||||
|
||||
- 权限 URI:`photographer_report`
|
||||
- 权限 URI:`report_photographer`
|
||||
- 首页路由:`HomeRoute.photographerReport`
|
||||
- 入口不固定置顶,依赖权限下发或用户手动添加到常用应用(`HomeCommonMenuStore.androidHomeMenuURIs` 白名单已包含该 URI)。
|
||||
|
||||
@ -57,10 +57,10 @@ flowchart TD
|
||||
|
||||
| 模块 | URI | 用途 | 后端 |
|
||||
| --- | --- | --- | --- |
|
||||
| WildPhotographerReport | `photographer_report` | 举报摄影师 | Mock |
|
||||
| WildPhotographerReport | `report_photographer` | 举报摄影师 | Mock |
|
||||
| LocationReport | `location_report` | 位置上报 | 真实 API |
|
||||
|
||||
## 测试要求
|
||||
|
||||
- `WildPhotographerReportTests`:Home/Submit/List ViewModel 校验与提交成功路径。
|
||||
- `HomeMenuRouterTests`:`photographer_report` 路由映射。
|
||||
- `HomeMenuRouterTests`:`report_photographer` 路由映射。
|
||||
|
||||
@ -147,6 +147,40 @@ final class AccountContextLoaderTests: XCTestCase {
|
||||
XCTAssertTrue(accountContext.storeScopes.isEmpty)
|
||||
XCTAssertNil(accountContext.currentStore)
|
||||
}
|
||||
|
||||
/// 测试门店接口失败时会保留登录/切换时已写入的门店作用域。
|
||||
func testRefreshSupplementalContextPreservesExistingStoreScopesWhenStoreAPIFails() async throws {
|
||||
let accountContext = AccountContext()
|
||||
accountContext.applyLogin(accountType: V9StoreUser.accountTypeValue)
|
||||
accountContext.replaceScopes(
|
||||
scenic: [BusinessScope(id: 88, name: "东湖景区", kind: .scenic)],
|
||||
stores: [BusinessScope(id: 66, name: "东湖门店", kind: .store, parentScenicId: 88)],
|
||||
currentScenicId: 88,
|
||||
currentStoreId: 66
|
||||
)
|
||||
let permissionContext = PermissionContext()
|
||||
let api = MockAccountContextAPI()
|
||||
api.roles = [
|
||||
RolePermissionResponse(
|
||||
role: RoleInfo(id: 7, name: "运营"),
|
||||
scenic: [ScenicInfo(id: 88, name: "东湖景区")]
|
||||
)
|
||||
]
|
||||
api.scenicResponse = ScenicListAllResponse(total: 1, list: [ScenicListItem(id: 88, name: "东湖景区")])
|
||||
api.storeError = APIError.networkFailed("门店接口失败")
|
||||
|
||||
try await AccountContextLoader().refreshSupplementalContext(
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
profileAPI: MockUserProfileAPI(),
|
||||
accountContextAPI: api,
|
||||
cachedCurrentScenicId: 88,
|
||||
cachedCurrentStoreId: 66
|
||||
)
|
||||
|
||||
XCTAssertEqual(accountContext.storeScopes.map(\.name), ["东湖门店"])
|
||||
XCTAssertEqual(accountContext.currentStore?.name, "东湖门店")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -46,6 +46,32 @@ final class AccountContextTests: XCTestCase {
|
||||
XCTAssertEqual(context.currentStore?.id, 20)
|
||||
}
|
||||
|
||||
/// 测试显式切换景区账号时会清空旧门店,而不是保留上一次门店选择。
|
||||
func testReplaceScopesExplicitSelectionClearsStoreForScenicAccount() {
|
||||
let context = AccountContext()
|
||||
context.replaceScopes(
|
||||
scenic: [BusinessScope(id: 128, name: "伊犁那拉提景区-5A", kind: .scenic)],
|
||||
stores: [BusinessScope(id: 37, name: "随心旅拍", kind: .store, parentScenicId: 128)],
|
||||
currentScenicId: 128,
|
||||
currentStoreId: 37,
|
||||
explicitSelection: true
|
||||
)
|
||||
XCTAssertEqual(context.currentStore?.name, "随心旅拍")
|
||||
|
||||
context.applyLogin(accountType: V9ScenicUser.accountTypeValue, businessUserId: 41, displayTitle: "伊犁那拉提景区-5A")
|
||||
context.replaceScopes(
|
||||
scenic: [BusinessScope(id: 128, name: "伊犁那拉提景区-5A", kind: .scenic)],
|
||||
stores: [BusinessScope(id: 37, name: "随心旅拍", kind: .store, parentScenicId: 128)],
|
||||
currentScenicId: 128,
|
||||
currentStoreId: nil,
|
||||
explicitSelection: true
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.accountType, V9ScenicUser.accountTypeValue)
|
||||
XCTAssertNil(context.currentStore)
|
||||
XCTAssertEqual(context.currentAccountDisplayTitle, "伊犁那拉提景区-5A")
|
||||
}
|
||||
|
||||
/// 测试缓存 ID 不存在时会回退到首个可用作用域。
|
||||
func testReplaceScopesFallsBackToFirstScopeWhenCachedIdIsMissing() {
|
||||
let context = AccountContext()
|
||||
|
||||
@ -81,6 +81,69 @@ final class AuthModelsTests: XCTestCase {
|
||||
XCTAssertEqual(response.primaryProfile?.displayName, "示例门店")
|
||||
}
|
||||
|
||||
/// 测试切换门店账号时会优先使用 isCurrent 标记校准景区和门店作用域。
|
||||
func testV9AuthResponsePreferredScopeSelectionUsesCurrentStoreAccount() throws {
|
||||
let scenicJSON = """
|
||||
{
|
||||
"account_type": "scenic_user",
|
||||
"id": 101,
|
||||
"user_id": 101,
|
||||
"scenic_user_id": 101,
|
||||
"ss_user_id": 101,
|
||||
"username": "scenic_admin",
|
||||
"real_name": "张三",
|
||||
"nickname": "张三",
|
||||
"phone": "13800138000",
|
||||
"scenic_id": 1,
|
||||
"scenic_name": "旧景区",
|
||||
"is_current": false
|
||||
}
|
||||
"""
|
||||
let storeJSON = """
|
||||
{
|
||||
"account_type": "store_user",
|
||||
"id": 201,
|
||||
"user_id": 201,
|
||||
"store_user_id": 201,
|
||||
"username": "store_admin",
|
||||
"user_name": "store_admin",
|
||||
"real_name": "李四",
|
||||
"phone": "13800138000",
|
||||
"avatar": "",
|
||||
"scenic_id": 2,
|
||||
"scenic_name": "新景区",
|
||||
"store_id": 20,
|
||||
"store_name": "新门店",
|
||||
"is_current": true
|
||||
}
|
||||
"""
|
||||
let scenicUser = try JSONDecoder().decode(V9ScenicUser.self, from: Data(scenicJSON.utf8))
|
||||
let storeUser = try JSONDecoder().decode(V9StoreUser.self, from: Data(storeJSON.utf8))
|
||||
let response = V9AuthResponse(token: "token", scenicUsers: [scenicUser], storeUsers: [storeUser])
|
||||
|
||||
XCTAssertEqual(response.currentAccountIdentity?.accountType, V9StoreUser.accountTypeValue)
|
||||
XCTAssertEqual(response.currentAccountIdentity?.businessUserId, 201)
|
||||
XCTAssertEqual(response.preferredScopeSelection.scenicId, 2)
|
||||
XCTAssertEqual(response.preferredScopeSelection.storeId, 20)
|
||||
|
||||
let context = AccountContext()
|
||||
context.replaceScopes(
|
||||
scenic: [BusinessScope(id: 1, name: "旧景区", kind: .scenic)],
|
||||
stores: [],
|
||||
currentScenicId: 1,
|
||||
currentStoreId: nil
|
||||
)
|
||||
context.replaceScopes(
|
||||
scenic: response.scenicScopes,
|
||||
stores: response.storeScopes,
|
||||
currentScenicId: response.preferredScopeSelection.scenicId,
|
||||
currentStoreId: response.preferredScopeSelection.storeId
|
||||
)
|
||||
|
||||
XCTAssertEqual(context.currentScenic?.id, 2)
|
||||
XCTAssertEqual(context.currentStore?.id, 20)
|
||||
}
|
||||
|
||||
/// 测试 LoginRequest 会填充后端要求的默认字段。
|
||||
func testLoginRequestEncodesExpectedDefaults() throws {
|
||||
let request = LoginRequest(username: "18651857230", password: "secret123")
|
||||
|
||||
@ -16,14 +16,14 @@ final class HomeAllFunctionsBuilderTests: XCTestCase {
|
||||
topLevelPermissions: [
|
||||
permission(name: "基本信息", uri: "basic_info"),
|
||||
permission(name: "位置上报", uri: "location_report"),
|
||||
permission(name: "举报摄影师", uri: "photographer_report"),
|
||||
permission(name: "举报摄影师", uri: "report_photographer"),
|
||||
permission(name: "相册管理", uri: "album_list"),
|
||||
permission(name: "我的钱包", uri: "wallet")
|
||||
],
|
||||
commonURIs: []
|
||||
)
|
||||
|
||||
XCTAssertEqual(snapshot.allFunctions.map(\.uri), ["location_report", "photographer_report", "wallet"])
|
||||
XCTAssertEqual(snapshot.allFunctions.map(\.uri), ["location_report", "report_photographer", "wallet"])
|
||||
}
|
||||
|
||||
/// 测试保持 API 顶层顺序,不使用 preferredOrder 重排。
|
||||
|
||||
@ -269,20 +269,20 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
XCTAssertEqual(uris, ["wallet", "message_center"])
|
||||
}
|
||||
|
||||
/// 测试 photographer_report 属于 Android menuList 白名单,且可加入常用应用。
|
||||
/// 测试 report_photographer 属于 Android menuList 白名单,且可加入常用应用。
|
||||
func testPhotographerReportIsWhitelistedAndCanBeAddedToCommon() {
|
||||
XCTAssertTrue(HomeCommonMenuStore.androidHomeMenuURIs.contains("photographer_report"))
|
||||
XCTAssertTrue(HomeCommonMenuStore.androidHomeMenuURIs.contains("report_photographer"))
|
||||
|
||||
let store = HomeCommonMenuStore(defaults: makeIsolatedDefaults())
|
||||
let uris = store.add(
|
||||
"photographer_report",
|
||||
"report_photographer",
|
||||
current: ["location_report"],
|
||||
topLevelPermissionURIs: ["location_report", "photographer_report"],
|
||||
topLevelPermissionURIs: ["location_report", "report_photographer"],
|
||||
roleCode: "photographer",
|
||||
accountScope: "scenic_user_photographer_report"
|
||||
accountScope: "scenic_user_report_photographer"
|
||||
)
|
||||
|
||||
XCTAssertEqual(uris, ["location_report", "photographer_report"])
|
||||
XCTAssertEqual(uris, ["location_report", "report_photographer"])
|
||||
}
|
||||
|
||||
/// 测试非白名单顶层项占用前 4 名额时,默认常用不会用后续白名单项补位。
|
||||
|
||||
@ -47,7 +47,7 @@ final class HomeMenuRouterTests: XCTestCase {
|
||||
("checkin_points", .destination(.punchPointList)),
|
||||
("location_report", .destination(.locationReport)),
|
||||
("location_report_history", .destination(.locationReportHistory)),
|
||||
("photographer_report", .destination(.photographerReport)),
|
||||
("report_photographer", .destination(.photographerReport)),
|
||||
("pm", .destination(.projectManagement)),
|
||||
("project_edit", .destination(.projectManagement)),
|
||||
("pm_manager", .destination(.pmProjectManagement)),
|
||||
@ -191,7 +191,7 @@ final class HomeMenuRouterTests: XCTestCase {
|
||||
XCTAssertTrue(uris.contains("sample_management"))
|
||||
XCTAssertTrue(uris.contains("checkin_points"))
|
||||
XCTAssertTrue(uris.contains("location_report"))
|
||||
XCTAssertTrue(uris.contains("photographer_report"))
|
||||
XCTAssertTrue(uris.contains("report_photographer"))
|
||||
XCTAssertTrue(uris.contains("pm"))
|
||||
XCTAssertTrue(uris.contains("pm_manager"))
|
||||
XCTAssertTrue(uris.contains("schedule_management"))
|
||||
@ -209,7 +209,7 @@ final class HomeMenuRouterTests: XCTestCase {
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "sample_upload", title: ""), .destination(.sampleUpload))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "checkin_points", title: ""), .destination(.punchPointList))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "location_report", title: ""), .destination(.locationReport))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "photographer_report", title: ""), .destination(.photographerReport))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "report_photographer", title: ""), .destination(.photographerReport))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm", title: ""), .destination(.projectManagement))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "pm_manager", title: ""), .destination(.pmProjectManagement))
|
||||
XCTAssertEqual(HomeMenuRouter.resolve(uri: "schedule_management", title: ""), .destination(.scheduleManagement))
|
||||
|
||||
Reference in New Issue
Block a user