将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.0 KiB
Swift
154 lines
5.0 KiB
Swift
//
|
||
// OperatingAreaViewModel.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import Foundation
|
||
import Combine
|
||
|
||
@MainActor
|
||
/// 运营区域 ViewModel,负责角色分流、围栏加载、解析和错误状态管理。
|
||
final class OperatingAreaViewModel: ObservableObject {
|
||
@Published private(set) var mode: OperatingAreaEntryMode?
|
||
@Published private(set) var loading = false
|
||
@Published private(set) var items: [OperatingAreaItem] = []
|
||
@Published private(set) var fenceRings: [OperatingFenceRing] = []
|
||
@Published private(set) var blockReason: OperatingAreaBlockReason?
|
||
@Published private(set) var errorMessage: String?
|
||
|
||
/// 按当前账号和角色刷新运营区域。
|
||
func reload(
|
||
api: any OperatingAreaServing,
|
||
accountContext: AccountContext,
|
||
permissionContext: PermissionContext
|
||
) async {
|
||
guard !loading else { return }
|
||
guard let nextMode = resolveMode(accountContext: accountContext, permissionContext: permissionContext) else {
|
||
return
|
||
}
|
||
|
||
mode = nextMode
|
||
loading = true
|
||
errorMessage = nil
|
||
blockReason = nil
|
||
defer { loading = false }
|
||
|
||
do {
|
||
let response: ListPayload<OperatingAreaItem>
|
||
switch nextMode {
|
||
case .storeAdmin(let storeId):
|
||
response = try await api.storeBusinessArea(storeId: storeId)
|
||
case .scenicAdmin(let scenicId):
|
||
response = try await api.scenicAdminBusinessArea(scenicId: scenicId)
|
||
}
|
||
apply(items: response.list, mode: nextMode)
|
||
} catch {
|
||
resetLoadedData()
|
||
let message = error.localizedDescription
|
||
errorMessage = message
|
||
blockReason = .backendMessage(message)
|
||
}
|
||
}
|
||
|
||
/// 根据当前上下文解析运营区域入口模式。
|
||
@discardableResult
|
||
func resolveMode(accountContext: AccountContext, permissionContext: PermissionContext) -> OperatingAreaEntryMode? {
|
||
let roleName = permissionContext.currentRole?.name.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||
let lowercaseRoleName = roleName.lowercased()
|
||
let isStoreRole = roleName.contains("店铺") || roleName.contains("门店") || lowercaseRoleName.contains("store")
|
||
let isScenicRole = roleName.contains("景区") || lowercaseRoleName.contains("scenic")
|
||
|
||
if isStoreRole {
|
||
guard let storeId = accountContext.currentStore?.id, storeId > 0 else {
|
||
setBlocked(.missingStore)
|
||
return nil
|
||
}
|
||
mode = .storeAdmin(storeId: storeId)
|
||
blockReason = nil
|
||
return mode
|
||
}
|
||
|
||
if isScenicRole {
|
||
guard let scenicId = accountContext.currentScenic?.id, scenicId > 0 else {
|
||
setBlocked(.missingScenic)
|
||
return nil
|
||
}
|
||
mode = .scenicAdmin(scenicId: scenicId)
|
||
blockReason = nil
|
||
return mode
|
||
}
|
||
|
||
if let storeId = accountContext.currentStore?.id, storeId > 0 {
|
||
mode = .storeAdmin(storeId: storeId)
|
||
blockReason = nil
|
||
return mode
|
||
}
|
||
|
||
if let scenicId = accountContext.currentScenic?.id, scenicId > 0 {
|
||
mode = .scenicAdmin(scenicId: scenicId)
|
||
blockReason = nil
|
||
return mode
|
||
}
|
||
|
||
setBlocked(.unsupportedRole)
|
||
return nil
|
||
}
|
||
|
||
/// 根据状态生成统计展示。
|
||
var summary: (areaCount: Int, fenceCount: Int, currentStoreFenceCount: Int) {
|
||
(
|
||
areaCount: items.count,
|
||
fenceCount: fenceRings.count,
|
||
currentStoreFenceCount: fenceRings.filter(\.isCurrentStore).count
|
||
)
|
||
}
|
||
|
||
/// 当前页面标题。
|
||
var title: String {
|
||
mode?.title ?? "运营区域"
|
||
}
|
||
|
||
private func apply(items: [OperatingAreaItem], mode: OperatingAreaEntryMode) {
|
||
self.items = items
|
||
if items.isEmpty {
|
||
fenceRings = []
|
||
blockReason = .emptyAreaList
|
||
return
|
||
}
|
||
|
||
let currentStoreId: Int? = {
|
||
guard case .storeAdmin(let storeId) = mode else { return nil }
|
||
return storeId
|
||
}()
|
||
|
||
let parsed = items.flatMap { item -> [OperatingFenceRing] in
|
||
OperatingAreaParser.parseToRings(item.businessMapArea).enumerated().compactMap { index, points in
|
||
guard points.count >= 3 else { return nil }
|
||
return OperatingFenceRing(
|
||
itemId: item.id,
|
||
regionName: item.name,
|
||
points: points,
|
||
isCurrentStore: currentStoreId == item.id,
|
||
ringIndex: index
|
||
)
|
||
}
|
||
}
|
||
fenceRings = parsed
|
||
blockReason = parsed.isEmpty ? .noParsableFenceData : nil
|
||
}
|
||
|
||
private func setBlocked(_ reason: OperatingAreaBlockReason) {
|
||
resetLoadedData()
|
||
blockReason = reason
|
||
errorMessage = nil
|
||
loading = false
|
||
}
|
||
|
||
private func resetLoadedData() {
|
||
items = []
|
||
fenceRings = []
|
||
}
|
||
}
|