Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.2 KiB
Swift
157 lines
5.2 KiB
Swift
//
|
||
// OperatingAreaViewModel.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
@MainActor
|
||
/// 运营区域 ViewModel,负责角色分流、围栏加载、解析和错误状态管理。
|
||
final class OperatingAreaViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var mode: OperatingAreaEntryMode? { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var items: [OperatingAreaItem] = [] { didSet { onChange?() } }
|
||
private(set) var fenceRings: [OperatingFenceRing] = [] { didSet { onChange?() } }
|
||
private(set) var blockReason: OperatingAreaBlockReason? { didSet { onChange?() } }
|
||
private(set) var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 按当前账号和角色刷新运营区域。
|
||
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 ?? "运营区域"
|
||
}
|
||
|
||
/// apply 业务逻辑。
|
||
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
|
||
}
|
||
|
||
/// setBlocked相关逻辑。
|
||
private func setBlocked(_ reason: OperatingAreaBlockReason) {
|
||
resetLoadedData()
|
||
blockReason = reason
|
||
errorMessage = nil
|
||
loading = false
|
||
}
|
||
|
||
/// 重置LoadedData状态。
|
||
private func resetLoadedData() {
|
||
items = []
|
||
fenceRings = []
|
||
}
|
||
}
|