将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.6 KiB
Swift
149 lines
4.6 KiB
Swift
//
|
||
// AccountContext.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/18.
|
||
//
|
||
|
||
import Combine
|
||
|
||
/// 当前账号的基础资料实体,用于跨页面展示昵称、手机号和头像。
|
||
struct AccountProfile: Codable, Equatable {
|
||
var userId: String
|
||
var displayName: String
|
||
var phone: String?
|
||
var avatarURL: String?
|
||
}
|
||
|
||
/// 当前业务作用域实体,表示景区或门店维度的上下文。
|
||
struct BusinessScope: Codable, Equatable, Identifiable {
|
||
/// 业务作用域类型,用于区分景区和门店。
|
||
enum Kind: Codable, Equatable {
|
||
case scenic
|
||
case store
|
||
}
|
||
|
||
var id: Int
|
||
var name: String
|
||
var kind: Kind
|
||
var parentScenicId: Int?
|
||
var status: Int?
|
||
var address: String?
|
||
var latitude: Double?
|
||
var longitude: Double?
|
||
var coverURLString: String?
|
||
|
||
/// 创建业务作用域,门店可通过 parentScenicId 标记所属景区。
|
||
init(
|
||
id: Int,
|
||
name: String,
|
||
kind: Kind,
|
||
parentScenicId: Int? = nil,
|
||
status: Int? = nil,
|
||
address: String? = nil,
|
||
latitude: Double? = nil,
|
||
longitude: Double? = nil,
|
||
coverURLString: String? = nil
|
||
) {
|
||
self.id = id
|
||
self.name = name
|
||
self.kind = kind
|
||
self.parentScenicId = parentScenicId
|
||
self.status = status
|
||
self.address = address
|
||
self.latitude = latitude
|
||
self.longitude = longitude
|
||
self.coverURLString = coverURLString
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 账号上下文状态中心,保存当前账号资料和景区/门店作用域。
|
||
final class AccountContext: ObservableObject {
|
||
@Published private(set) var profile: AccountProfile?
|
||
@Published private(set) var scenicScopes: [BusinessScope] = []
|
||
@Published private(set) var storeScopes: [BusinessScope] = []
|
||
@Published var currentScenic: BusinessScope?
|
||
@Published var currentStore: BusinessScope?
|
||
|
||
/// 应用登录成功后写入账号资料。
|
||
func applyLogin(profile: AccountProfile? = nil) {
|
||
self.profile = profile
|
||
}
|
||
|
||
/// 替换当前账号资料,通常用于用户信息刷新后同步全局展示。
|
||
func replaceProfile(_ profile: AccountProfile?) {
|
||
self.profile = profile
|
||
}
|
||
|
||
/// 替换景区和门店作用域,并尽量保持当前选择不变。
|
||
func replaceScopes(
|
||
scenic: [BusinessScope],
|
||
stores: [BusinessScope],
|
||
currentScenicId: Int? = nil,
|
||
currentStoreId: Int? = nil
|
||
) {
|
||
scenicScopes = scenic
|
||
storeScopes = stores
|
||
currentScenic = currentScenicId.flatMap { id in
|
||
scenic.first { $0.id == id }
|
||
} ?? currentScenic.flatMap { current in
|
||
scenic.first { $0.id == current.id }
|
||
} ?? scenic.first
|
||
currentStore = resolvedStore(
|
||
in: stores,
|
||
currentScenic: currentScenic,
|
||
preferredStoreId: currentStoreId ?? currentStore?.id
|
||
)
|
||
}
|
||
|
||
/// 切换当前景区,并按景区重新校准当前门店。
|
||
func selectScenic(id scenicId: Int) {
|
||
guard let scenic = scenicScopes.first(where: { $0.id == scenicId }) else { return }
|
||
currentScenic = scenic
|
||
currentStore = resolvedStore(
|
||
in: storeScopes,
|
||
currentScenic: scenic,
|
||
preferredStoreId: currentStore?.id
|
||
)
|
||
}
|
||
|
||
/// 切换当前门店,并在门店有关联景区时同步当前景区。
|
||
func selectStore(id storeId: Int) {
|
||
guard let store = storeScopes.first(where: { $0.id == storeId }) else { return }
|
||
currentStore = store
|
||
if let scenicId = store.parentScenicId,
|
||
currentScenic?.id != scenicId,
|
||
let scenic = scenicScopes.first(where: { $0.id == scenicId }) {
|
||
currentScenic = scenic
|
||
}
|
||
}
|
||
|
||
/// 清空账号上下文,通常在退出登录时调用。
|
||
func reset() {
|
||
profile = nil
|
||
scenicScopes = []
|
||
storeScopes = []
|
||
currentScenic = nil
|
||
currentStore = nil
|
||
}
|
||
|
||
/// 按当前景区和期望门店 ID 选择有效门店。
|
||
private func resolvedStore(
|
||
in stores: [BusinessScope],
|
||
currentScenic: BusinessScope?,
|
||
preferredStoreId: Int?
|
||
) -> BusinessScope? {
|
||
let scenicId = currentScenic?.id
|
||
let storesForScenic = stores.filter { store in
|
||
guard let scenicId else { return true }
|
||
return store.parentScenicId == nil || store.parentScenicId == scenicId
|
||
}
|
||
if let preferredStoreId,
|
||
let store = storesForScenic.first(where: { $0.id == preferredStoreId }) {
|
||
return store
|
||
}
|
||
return storesForScenic.first
|
||
}
|
||
}
|