Implement permission-driven home tab aligned with Android.
Load role-permission on first visit to drive menus, role-based layout, store card, and location reporting with account-scoped persistence and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
66
suixinkan/Features/Home/API/HomeAPI.swift
Normal file
66
suixinkan/Features/Home/API/HomeAPI.swift
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// HomeAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 首页 API,封装权限、门店与位置上报相关接口。
|
||||
final class HomeAPI {
|
||||
private let client: APIClient
|
||||
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 拉取当前账号全部角色权限配置。
|
||||
func rolePermissions() async throws -> [RolePermissionResponse] {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/yf-handset-app/role-permission")
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取全部门店列表。
|
||||
func storeList() async throws -> StoreListResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/app/store/all")
|
||||
)
|
||||
}
|
||||
|
||||
/// 拉取摄影师位置上报详情。
|
||||
func locationDetail(staffId: String) async throws -> LocationDetailResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/loacation/detail",
|
||||
queryItems: [URLQueryItem(name: "staff_id", value: staffId)]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 上报摄影师位置。
|
||||
func reportLocation(
|
||||
staffId: String,
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
address: String,
|
||||
type: Int,
|
||||
scenicId: String
|
||||
) async throws -> LocationReportResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/loacation/report",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "staff_id", value: staffId),
|
||||
URLQueryItem(name: "latitude", value: String(latitude)),
|
||||
URLQueryItem(name: "longitude", value: String(longitude)),
|
||||
URLQueryItem(name: "address", value: address),
|
||||
URLQueryItem(name: "type", value: String(type)),
|
||||
URLQueryItem(name: "scenic_id", value: scenicId),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
28
suixinkan/Features/Home/Models/HomeMenuItem.swift
Normal file
28
suixinkan/Features/Home/Models/HomeMenuItem.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// HomeMenuItem.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 首页菜单展示项,由服务端权限 URI 与本地 catalog 合并而成。
|
||||
struct HomeMenuItem: Equatable, Hashable, Sendable {
|
||||
let uri: String
|
||||
let title: String
|
||||
let iconName: String
|
||||
let serverName: String
|
||||
|
||||
init(uri: String, title: String, iconName: String, serverName: String = "") {
|
||||
self.uri = uri
|
||||
self.title = title
|
||||
self.iconName = iconName
|
||||
self.serverName = serverName
|
||||
}
|
||||
|
||||
/// 固定的「更多功能」入口。
|
||||
static let moreFunctions = HomeMenuItem(
|
||||
uri: "more_functions",
|
||||
title: "更多功能",
|
||||
iconName: "square.grid.2x2"
|
||||
)
|
||||
}
|
||||
38
suixinkan/Features/Home/Models/LocationModels.swift
Normal file
38
suixinkan/Features/Home/Models/LocationModels.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// LocationModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 位置上报详情,对齐 Android `LocationDetailResponse`。
|
||||
struct LocationDetailResponse: Decodable, Sendable {
|
||||
let status: Int
|
||||
let needReport: Bool
|
||||
let expireTime: Int64
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case status
|
||||
case needReport = "need_report"
|
||||
case expireTime = "expire_time"
|
||||
}
|
||||
|
||||
init(status: Int = 0, needReport: Bool = true, expireTime: Int64 = 0) {
|
||||
self.status = status
|
||||
self.needReport = needReport
|
||||
self.expireTime = expireTime
|
||||
}
|
||||
}
|
||||
|
||||
/// 位置上报结果,对齐 Android `LocationReportResponse`。
|
||||
struct LocationReportResponse: Decodable, Sendable {
|
||||
let staffId: String
|
||||
let expired: Int64
|
||||
let status: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case staffId = "staff_id"
|
||||
case expired
|
||||
case status
|
||||
}
|
||||
}
|
||||
143
suixinkan/Features/Home/Models/RolePermissionModels.swift
Normal file
143
suixinkan/Features/Home/Models/RolePermissionModels.swift
Normal file
@ -0,0 +1,143 @@
|
||||
//
|
||||
// RolePermissionModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 角色权限接口根节点,对齐 Android `RolePermissionResponse`。
|
||||
struct RolePermissionResponse: Codable, Equatable, Sendable {
|
||||
let role: RoleInfo
|
||||
let scenic: [ScenicInfo]
|
||||
|
||||
init(role: RoleInfo, scenic: [ScenicInfo] = []) {
|
||||
self.role = role
|
||||
self.scenic = scenic
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色信息与顶层权限列表。
|
||||
struct RoleInfo: Codable, Equatable, Sendable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let roleCode: String
|
||||
let notes: String?
|
||||
let permission: [PermissionItem]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case roleCode = "role_code"
|
||||
case notes
|
||||
case permission
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
name: String = "",
|
||||
roleCode: String = "",
|
||||
notes: String? = nil,
|
||||
permission: [PermissionItem] = []
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.roleCode = roleCode
|
||||
self.notes = notes
|
||||
self.permission = permission
|
||||
}
|
||||
}
|
||||
|
||||
/// 权限树节点。
|
||||
struct PermissionItem: Codable, Equatable, Sendable {
|
||||
let id: Int
|
||||
let pid: Int
|
||||
let name: String
|
||||
let uri: String
|
||||
let iconSrc: String?
|
||||
let children: [PermissionItem]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case pid
|
||||
case name
|
||||
case uri
|
||||
case iconSrc = "icon_src"
|
||||
case children
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
pid: Int = 0,
|
||||
name: String = "",
|
||||
uri: String = "",
|
||||
iconSrc: String? = nil,
|
||||
children: [PermissionItem]? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.pid = pid
|
||||
self.name = name
|
||||
self.uri = uri
|
||||
self.iconSrc = iconSrc
|
||||
self.children = children
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色绑定的景区信息。
|
||||
struct ScenicInfo: Codable, Equatable, Sendable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let status: Int
|
||||
let location: ScenicLocationInfo?
|
||||
let coverImg: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case status
|
||||
case location
|
||||
case coverImg = "cover_img"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
name: String = "",
|
||||
status: Int = 0,
|
||||
location: ScenicLocationInfo? = nil,
|
||||
coverImg: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.location = location
|
||||
self.coverImg = coverImg
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区位置信息。
|
||||
struct ScenicLocationInfo: Codable, Equatable, Sendable, Hashable {
|
||||
let lng: Double
|
||||
let lat: Double
|
||||
let address: String
|
||||
}
|
||||
|
||||
/// 扁平化后的权限项,供首页菜单与本地缓存使用。
|
||||
struct HomePermissionItem: Codable, Equatable, Sendable, Hashable {
|
||||
let id: String
|
||||
let name: String
|
||||
let uri: String
|
||||
let pid: Int
|
||||
|
||||
init(id: String, name: String, uri: String, pid: Int = 0) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.uri = uri
|
||||
self.pid = pid
|
||||
}
|
||||
|
||||
init(from item: PermissionItem) {
|
||||
self.id = String(item.id)
|
||||
self.name = item.name
|
||||
self.uri = item.uri
|
||||
self.pid = item.pid
|
||||
}
|
||||
}
|
||||
52
suixinkan/Features/Home/Models/StoreModels.swift
Normal file
52
suixinkan/Features/Home/Models/StoreModels.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// StoreModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 门店列表项,对齐 Android `StoreItem`。
|
||||
struct StoreItem: Codable, Equatable, Sendable {
|
||||
let id: Int
|
||||
let scenicId: Int
|
||||
let name: String
|
||||
let address: String
|
||||
let status: Int
|
||||
let statusText: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case address
|
||||
case status
|
||||
case statusText = "status_text"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
scenicId: Int = 0,
|
||||
name: String = "",
|
||||
address: String = "",
|
||||
status: Int = 0,
|
||||
statusText: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.scenicId = scenicId
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.status = status
|
||||
self.statusText = statusText
|
||||
}
|
||||
}
|
||||
|
||||
/// 门店列表响应,对齐 Android `ListResponse<StoreItem>`。
|
||||
struct StoreListResponse: Codable, Sendable {
|
||||
let total: Int
|
||||
let list: [StoreItem]
|
||||
|
||||
init(total: Int = 0, list: [StoreItem] = []) {
|
||||
self.total = total
|
||||
self.list = list
|
||||
}
|
||||
}
|
||||
50
suixinkan/Features/Home/Services/HomeCommonMenuStore.swift
Normal file
50
suixinkan/Features/Home/Services/HomeCommonMenuStore.swift
Normal file
@ -0,0 +1,50 @@
|
||||
//
|
||||
// HomeCommonMenuStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 常用应用 URI 持久化,按账号与角色作用域存储。
|
||||
final class HomeCommonMenuStore {
|
||||
|
||||
private let defaults: UserDefaults
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
}
|
||||
|
||||
/// 读取已保存的常用 URI;无记录时返回空数组。
|
||||
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
||||
defaults.stringArray(forKey: storageKey(accountScope: accountScope, roleCode: roleCode)) ?? []
|
||||
}
|
||||
|
||||
/// 保存常用 URI 列表。
|
||||
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
||||
defaults.set(uris, forKey: storageKey(accountScope: accountScope, roleCode: roleCode))
|
||||
}
|
||||
|
||||
/// 根据权限生成默认常用 URI(最多 4 个)。
|
||||
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
||||
let uris = permissions.map(\.uri)
|
||||
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
||||
}
|
||||
|
||||
/// 构建常用菜单列表。
|
||||
func commonMenus(
|
||||
from allMenus: [HomeMenuItem],
|
||||
permissions: [HomePermissionItem],
|
||||
accountScope: String,
|
||||
roleCode: String
|
||||
) -> [HomeMenuItem] {
|
||||
let saved = savedCommonURIs(accountScope: accountScope, roleCode: roleCode)
|
||||
let defaultURIs = Self.defaultCommonURIs(from: permissions)
|
||||
let commonURIs = saved.isEmpty ? defaultURIs : saved
|
||||
let commonSet = Set(commonURIs)
|
||||
return allMenus.filter { commonSet.contains($0.uri) }
|
||||
}
|
||||
|
||||
private func storageKey(accountScope: String, roleCode: String) -> String {
|
||||
"\(accountScope)_role_\(roleCode)_common_uris"
|
||||
}
|
||||
}
|
||||
97
suixinkan/Features/Home/Services/HomeLocationProvider.swift
Normal file
97
suixinkan/Features/Home/Services/HomeLocationProvider.swift
Normal file
@ -0,0 +1,97 @@
|
||||
//
|
||||
// HomeLocationProvider.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
|
||||
/// 首页位置采集结果。
|
||||
struct HomeLocationSnapshot: Sendable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let address: String
|
||||
}
|
||||
|
||||
/// 首页位置采集,基于 CoreLocation 并提供可选逆地理。
|
||||
final class HomeLocationProvider: NSObject, CLLocationManagerDelegate {
|
||||
|
||||
private let manager = CLLocationManager()
|
||||
private var coordinateContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
/// 请求当前位置与地址。
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
let status = manager.authorizationStatus
|
||||
if status == .notDetermined {
|
||||
manager.requestWhenInUseAuthorization()
|
||||
try await Task.sleep(nanoseconds: 300_000_000)
|
||||
}
|
||||
|
||||
let resolvedStatus = manager.authorizationStatus
|
||||
guard resolvedStatus == .authorizedWhenInUse || resolvedStatus == .authorizedAlways else {
|
||||
throw HomeLocationProviderError.permissionDenied
|
||||
}
|
||||
|
||||
let coordinate = try await requestCoordinate()
|
||||
let address = await reverseGeocode(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
return HomeLocationSnapshot(
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude,
|
||||
address: address
|
||||
)
|
||||
}
|
||||
|
||||
private func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
self.coordinateContinuation = continuation
|
||||
manager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
private func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||
let geocoder = CLGeocoder()
|
||||
do {
|
||||
let placemarks = try await geocoder.reverseGeocodeLocation(location)
|
||||
return placemarks.first?.formattedAddress ?? ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
coordinateContinuation?.resume(returning: location.coordinate)
|
||||
coordinateContinuation = nil
|
||||
}
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
coordinateContinuation?.resume(throwing: error)
|
||||
coordinateContinuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页定位错误。
|
||||
enum HomeLocationProviderError: LocalizedError {
|
||||
case permissionDenied
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .permissionDenied: "需要位置权限才能上线"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CLPlacemark {
|
||||
var formattedAddress: String {
|
||||
[subLocality, locality, administrativeArea]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "")
|
||||
}
|
||||
}
|
||||
139
suixinkan/Features/Home/Services/HomeLocationStateStore.swift
Normal file
139
suixinkan/Features/Home/Services/HomeLocationStateStore.swift
Normal file
@ -0,0 +1,139 @@
|
||||
//
|
||||
// HomeLocationStateStore.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 首页在线状态与 2 小时倒计时存储,对齐 Android `LocationStateRepository`。
|
||||
final class HomeLocationStateStore {
|
||||
|
||||
static let onlineDurationMillis: Int64 = 2 * 60 * 60 * 1000
|
||||
|
||||
private(set) var isOnline = false
|
||||
private(set) var elapsedSeconds: Int64 = 0
|
||||
private(set) var nextReportCountdownSeconds: Int64 = 0
|
||||
private(set) var reminderMinutes = 0
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
private let store: AppStore
|
||||
private var countdownTask: Task<Void, Never>?
|
||||
private var anchorTimestamp: Int64?
|
||||
|
||||
init(store: AppStore = .shared) {
|
||||
self.store = store
|
||||
reminderMinutes = store.locationReminderMinutes
|
||||
isOnline = store.onlineStatus
|
||||
}
|
||||
|
||||
/// 启动时恢复在线倒计时。
|
||||
func restoreStateIfNeeded() {
|
||||
guard store.onlineStatus else { return }
|
||||
let lastTime = store.lastLocationReportTime
|
||||
guard lastTime > 0 else { return }
|
||||
|
||||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let elapsed = now - lastTime
|
||||
if elapsed >= Self.onlineDurationMillis {
|
||||
updateOnlineStatus(false)
|
||||
store.clearLastLocationReportTime()
|
||||
notifyChange()
|
||||
return
|
||||
}
|
||||
|
||||
anchorTimestamp = lastTime
|
||||
let remaining = Self.onlineDurationMillis - elapsed
|
||||
elapsedSeconds = max(0, elapsed / 1000)
|
||||
nextReportCountdownSeconds = max(0, remaining / 1000)
|
||||
startCountdown(anchorTime: lastTime)
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
/// 更新在线状态并持久化。
|
||||
func updateOnlineStatus(_ online: Bool) {
|
||||
isOnline = online
|
||||
store.onlineStatus = online
|
||||
if !online {
|
||||
stopCountdown()
|
||||
store.clearLastLocationReportTime()
|
||||
}
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
/// 更新提醒分钟数。
|
||||
func updateReminderMinutes(_ minutes: Int) {
|
||||
reminderMinutes = minutes
|
||||
store.locationReminderMinutes = minutes
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
/// 开始新的位置上报会话。
|
||||
func startLocationReport(at timestamp: Int64 = Int64(Date().timeIntervalSince1970 * 1000)) {
|
||||
anchorTimestamp = timestamp
|
||||
store.lastLocationReportTime = timestamp
|
||||
isOnline = true
|
||||
store.onlineStatus = true
|
||||
elapsedSeconds = 0
|
||||
nextReportCountdownSeconds = Self.onlineDurationMillis / 1000
|
||||
startCountdown(anchorTime: timestamp)
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
/// 格式化倒计时文本 `HH:MM:SS`。
|
||||
func formattedCountdown(from seconds: Int64) -> String {
|
||||
let hours = seconds / 3600
|
||||
let minutes = (seconds % 3600) / 60
|
||||
let secs = seconds % 60
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, secs)
|
||||
}
|
||||
|
||||
var countdownDisplayText: String {
|
||||
formattedCountdown(from: nextReportCountdownSeconds)
|
||||
}
|
||||
|
||||
/// 是否到达提前提醒窗口。
|
||||
func shouldTriggerTimeoutReminder() -> Bool {
|
||||
guard reminderMinutes > 0, isOnline else { return false }
|
||||
let reminderSeconds = Int64(reminderMinutes * 60)
|
||||
return nextReportCountdownSeconds > 0 && nextReportCountdownSeconds <= reminderSeconds
|
||||
}
|
||||
|
||||
private func startCountdown(anchorTime: Int64) {
|
||||
countdownTask?.cancel()
|
||||
countdownTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
guard let self else { return }
|
||||
guard self.isOnline else { return }
|
||||
|
||||
let now = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let elapsed = now - anchorTime
|
||||
let remaining = Self.onlineDurationMillis - elapsed
|
||||
let elapsedSeconds = max(0, elapsed / 1000)
|
||||
|
||||
self.elapsedSeconds = elapsedSeconds
|
||||
if remaining <= 0 {
|
||||
self.nextReportCountdownSeconds = 0
|
||||
self.updateOnlineStatus(false)
|
||||
self.store.clearLastLocationReportTime()
|
||||
return
|
||||
}
|
||||
self.nextReportCountdownSeconds = max(0, remaining / 1000)
|
||||
self.notifyChange()
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCountdown() {
|
||||
countdownTask?.cancel()
|
||||
countdownTask = nil
|
||||
anchorTimestamp = nil
|
||||
elapsedSeconds = 0
|
||||
nextReportCountdownSeconds = 0
|
||||
}
|
||||
|
||||
private func notifyChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
58
suixinkan/Features/Home/Services/HomeMenuCatalog.swift
Normal file
58
suixinkan/Features/Home/Services/HomeMenuCatalog.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// HomeMenuCatalog.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
||||
enum HomeMenuCatalog {
|
||||
|
||||
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
||||
static let allItems: [HomeMenuItem] = [
|
||||
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "gearshape"),
|
||||
HomeMenuItem(uri: "wallet", title: "我的钱包", iconName: "wallet.pass"),
|
||||
HomeMenuItem(uri: "cloud_management", title: "相册云盘", iconName: "icloud"),
|
||||
HomeMenuItem(uri: "asset_management", title: "素材管理", iconName: "photo.on.rectangle"),
|
||||
HomeMenuItem(uri: "task_management", title: "任务管理", iconName: "checklist"),
|
||||
HomeMenuItem(uri: "task_management_editor", title: "任务管理", iconName: "checklist"),
|
||||
HomeMenuItem(uri: "schedule_management", title: "日程管理", iconName: "calendar"),
|
||||
HomeMenuItem(uri: "system_settings", title: "设置中心", iconName: "slider.horizontal.3"),
|
||||
HomeMenuItem(uri: "message_center", title: "消息中心", iconName: "bell"),
|
||||
HomeMenuItem(uri: "checkin_points", title: "打卡点管理", iconName: "mappin.and.ellipse"),
|
||||
HomeMenuItem(uri: "sample_management", title: "样片管理", iconName: "photo.stack"),
|
||||
HomeMenuItem(uri: "live_stream_management", title: "直播管理", iconName: "video"),
|
||||
HomeMenuItem(uri: "verification_order", title: "核销订单", iconName: "checkmark.seal"),
|
||||
HomeMenuItem(uri: "live_album", title: "直播相册", iconName: "photo.on.rectangle.angled"),
|
||||
HomeMenuItem(uri: "pm", title: "项目管理", iconName: "folder"),
|
||||
HomeMenuItem(uri: "pm_manager", title: "项目管理", iconName: "folder"),
|
||||
HomeMenuItem(uri: "location_report", title: "位置上报", iconName: "location"),
|
||||
HomeMenuItem(uri: "registration_invitation", title: "注册邀请", iconName: "person.badge.plus"),
|
||||
HomeMenuItem(uri: "store", title: "店铺管理", iconName: "building.2"),
|
||||
HomeMenuItem(uri: "fly", title: "飞行管理", iconName: "airplane"),
|
||||
HomeMenuItem(uri: "pilot_cert", title: "飞手认证", iconName: "person.text.rectangle"),
|
||||
HomeMenuItem(uri: "pilot_controller", title: "飞控", iconName: "gamecontroller"),
|
||||
HomeMenuItem(uri: "/scenic-queue", title: "排队管理", iconName: "person.3"),
|
||||
HomeMenuItem(uri: "operating-area", title: "运营区域", iconName: "map"),
|
||||
HomeMenuItem(uri: "/scenic-order-manage", title: "景区订单", iconName: "doc.text"),
|
||||
HomeMenuItem(uri: "cooperation_order", title: "合作订单", iconName: "person.2"),
|
||||
]
|
||||
|
||||
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
||||
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
||||
permissions.compactMap { permission in
|
||||
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
||||
return HomeMenuItem(
|
||||
uri: catalog.uri,
|
||||
title: catalog.title,
|
||||
iconName: catalog.iconName,
|
||||
serverName: permission.name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找 catalog 项。
|
||||
static func item(for uri: String) -> HomeMenuItem? {
|
||||
allItems.first { $0.uri == uri }
|
||||
}
|
||||
}
|
||||
108
suixinkan/Features/Home/Services/HomeRouteHandler.swift
Normal file
108
suixinkan/Features/Home/Services/HomeRouteHandler.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// HomeRouteHandler.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 首页菜单路由,将 permission URI 映射到已有页面或提示。
|
||||
enum HomeRouteHandler {
|
||||
|
||||
/// 处理菜单点击。
|
||||
@MainActor
|
||||
static func open(
|
||||
uri: String,
|
||||
from viewController: UIViewController,
|
||||
storeItem: StoreItem?
|
||||
) {
|
||||
if uri == "pilot_controller" || uri == "more_functions" {
|
||||
showDeveloping(from: viewController)
|
||||
return
|
||||
}
|
||||
|
||||
if uri != "pilot_controller", AppStore.shared.currentScenicId <= 0 {
|
||||
showToast("请先选择景区", from: viewController)
|
||||
return
|
||||
}
|
||||
|
||||
switch uri {
|
||||
case "verification_order":
|
||||
selectTab(.orders, from: viewController)
|
||||
case "photographer_stats":
|
||||
selectTab(.statistics, from: viewController)
|
||||
case "system_settings", "space_settings":
|
||||
selectTab(.profile, from: viewController)
|
||||
case "pilot_cert":
|
||||
let controller = RealNameAuthViewController()
|
||||
viewController.navigationController?.pushViewController(controller, animated: true)
|
||||
case "operating-area":
|
||||
handleOperatingArea(from: viewController, storeItem: storeItem)
|
||||
case "cooperation_order":
|
||||
if !hasCooperationOrderPermission() {
|
||||
showToast("暂无合作订单权限", from: viewController)
|
||||
return
|
||||
}
|
||||
showDeveloping(from: viewController)
|
||||
default:
|
||||
showDeveloping(from: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func handleOperatingArea(from viewController: UIViewController, storeItem: StoreItem?) {
|
||||
guard let role = AppStore.shared.currentAppRole else {
|
||||
showToast("当前账号暂不支持运营区域", from: viewController)
|
||||
return
|
||||
}
|
||||
switch role {
|
||||
case .storeAdmin:
|
||||
guard let storeItem, storeItem.id > 0 else {
|
||||
showToast("请先选择店铺", from: viewController)
|
||||
return
|
||||
}
|
||||
showDeveloping(from: viewController)
|
||||
case .scenicAdmin:
|
||||
guard AppStore.shared.currentScenicId > 0 else {
|
||||
showToast("请先选择景区", from: viewController)
|
||||
return
|
||||
}
|
||||
showDeveloping(from: viewController)
|
||||
default:
|
||||
showToast("当前账号暂不支持运营区域", from: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
private static func hasCooperationOrderPermission() -> Bool {
|
||||
let permissions = AppStore.shared.permissionItems()
|
||||
return permissions.contains { $0.uri == "cooperation_order" }
|
||||
|| permissions.contains(where: { containsCooperationOrder(in: $0) })
|
||||
}
|
||||
|
||||
private static func containsCooperationOrder(in item: HomePermissionItem) -> Bool {
|
||||
item.uri == "cooperation_order"
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func selectTab(_ tab: AppTab, from viewController: UIViewController) {
|
||||
guard let tabBar = viewController.tabBarController as? MainTabBarController else { return }
|
||||
tabBar.selectTab(tab)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func showDeveloping(from viewController: UIViewController) {
|
||||
showToast("功能开发中", from: viewController)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func showToast(_ message: String, from viewController: UIViewController) {
|
||||
if let base = viewController as? BaseViewController {
|
||||
base.showToast(message)
|
||||
return
|
||||
}
|
||||
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
|
||||
viewController.present(alert, animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
alert.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
suixinkan/Features/Home/Services/RolePermissionMatcher.swift
Normal file
39
suixinkan/Features/Home/Services/RolePermissionMatcher.swift
Normal file
@ -0,0 +1,39 @@
|
||||
//
|
||||
// RolePermissionMatcher.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 角色权限匹配器,在 `role-permission` 列表中定位当前账号角色。
|
||||
enum RolePermissionMatcher {
|
||||
|
||||
/// 按 role_code → legacy id → role_name 优先级匹配权限项。
|
||||
static func match(
|
||||
in list: [RolePermissionResponse],
|
||||
roleCode: String,
|
||||
roleName: String,
|
||||
legacyRoleId: Int = 0
|
||||
) -> RolePermissionResponse? {
|
||||
if let role = AppRoleCode.fromCode(roleCode),
|
||||
let matched = list.first(where: { AppRoleCode.fromCode($0.role.roleCode) == role }) {
|
||||
return matched
|
||||
}
|
||||
|
||||
if legacyRoleId > 0,
|
||||
let matched = list.first(where: { $0.role.id == legacyRoleId }) {
|
||||
return matched
|
||||
}
|
||||
|
||||
let trimmedName = roleName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedName.isEmpty {
|
||||
return list.first { item in
|
||||
let itemName = item.role.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !itemName.isEmpty else { return false }
|
||||
return itemName.contains(trimmedName) || trimmedName.contains(itemName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
418
suixinkan/Features/Home/ViewModels/HomeViewModel.swift
Normal file
418
suixinkan/Features/Home/ViewModels/HomeViewModel.swift
Normal file
@ -0,0 +1,418 @@
|
||||
//
|
||||
// HomeViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 首页 ViewModel,对齐 Android `HomeViewModel` 的权限、门店与位置逻辑。
|
||||
final class HomeViewModel {
|
||||
|
||||
private(set) var currentScenicName = ""
|
||||
private(set) var currentAppRole: AppRoleCode?
|
||||
private(set) var commonMenus: [HomeMenuItem] = []
|
||||
private(set) var storeItem: StoreItem?
|
||||
private(set) var showPermissionDialog = false
|
||||
private(set) var showScenicDialog = false
|
||||
private(set) var showLocationTimeoutDialog = false
|
||||
private(set) var showOnlineStatusDialog = false
|
||||
private(set) var showLocationReportSuccessDialog = false
|
||||
private(set) var isMinimalTopRole = false
|
||||
private(set) var locationInfoText = "定位中..."
|
||||
private(set) var reportTimeText = ""
|
||||
private(set) var isLocationReporting = false
|
||||
private(set) var needsPermissionReload = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
let locationStateStore: HomeLocationStateStore
|
||||
private let appStore: AppStore
|
||||
private let commonMenuStore: HomeCommonMenuStore
|
||||
private let locationProvider: HomeLocationProvider
|
||||
|
||||
private var lastOnlineStatusSwitchTime: Int64 = 0
|
||||
private var lastLocationReportTime: Int64 = 0
|
||||
private var timeoutDialogTriggered = false
|
||||
private var dismissedLocationTimeoutDialog = false
|
||||
private let operationIntervalMillis: Int64 = 30_000
|
||||
|
||||
init(
|
||||
appStore: AppStore = .shared,
|
||||
locationStateStore: HomeLocationStateStore = HomeLocationStateStore(),
|
||||
commonMenuStore: HomeCommonMenuStore = HomeCommonMenuStore(),
|
||||
locationProvider: HomeLocationProvider = HomeLocationProvider()
|
||||
) {
|
||||
self.appStore = appStore
|
||||
self.locationStateStore = locationStateStore
|
||||
self.commonMenuStore = commonMenuStore
|
||||
self.locationProvider = locationProvider
|
||||
self.locationStateStore.onStateChange = { [weak self] in
|
||||
self?.notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
var isOnline: Bool { locationStateStore.isOnline }
|
||||
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
||||
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
||||
|
||||
/// 首次进入首页时加载权限并恢复在线状态。
|
||||
func initialize(api: HomeAPI) async {
|
||||
locationStateStore.restoreStateIfNeeded()
|
||||
refreshLocalDisplayState()
|
||||
await loadPermissions(api: api, force: true)
|
||||
}
|
||||
|
||||
/// 账号切换后标记需要重新拉取权限。
|
||||
func markNeedsPermissionReload() {
|
||||
needsPermissionReload = true
|
||||
}
|
||||
|
||||
/// 按需重新拉取权限。
|
||||
func reloadIfNeeded(api: HomeAPI) async {
|
||||
guard needsPermissionReload else {
|
||||
refreshLocalDisplayState()
|
||||
rebuildCommonMenus()
|
||||
await loadStoreListIfNeeded(api: api)
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
needsPermissionReload = false
|
||||
await loadPermissions(api: api, force: true)
|
||||
}
|
||||
|
||||
/// 拉取 role-permission 并刷新菜单。
|
||||
func loadPermissions(api: HomeAPI, force: Bool = false) async {
|
||||
if !force, !appStore.permissionItems().isEmpty {
|
||||
rebuildCommonMenus()
|
||||
await loadStoreListIfNeeded(api: api)
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let rolePermissionList = try await api.rolePermissions()
|
||||
if rolePermissionList.isEmpty {
|
||||
appStore.savePermissionItems([])
|
||||
showPermissionDialog = true
|
||||
rebuildCommonMenus()
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
appStore.saveRolePermissionList(rolePermissionList)
|
||||
let matched = appStore.matchRolePermissionItem(in: rolePermissionList) ?? rolePermissionList[0]
|
||||
if !matched.role.name.isEmpty {
|
||||
appStore.roleName = matched.role.name
|
||||
}
|
||||
appStore.saveRoleScenicList(matched.scenic)
|
||||
let permissions = matched.role.permission.map(HomePermissionItem.init)
|
||||
appStore.savePermissionItems(permissions)
|
||||
showPermissionDialog = false
|
||||
refreshLocalDisplayState()
|
||||
rebuildCommonMenus()
|
||||
await loadStoreListIfNeeded(api: api)
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
appStore.savePermissionItems([])
|
||||
showPermissionDialog = true
|
||||
rebuildCommonMenus()
|
||||
onShowMessage?(error.localizedDescription)
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺管理员加载当前景区门店。
|
||||
func loadStoreListIfNeeded(api: HomeAPI) async {
|
||||
guard currentAppRole == .storeAdmin else {
|
||||
storeItem = nil
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
guard appStore.currentScenicId > 0 else {
|
||||
storeItem = nil
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await api.storeList()
|
||||
let scenicId = appStore.currentScenicId
|
||||
let savedStoreId = appStore.currentStoreId
|
||||
let savedMatch = savedStoreId > 0 ? response.list.first { $0.id == savedStoreId } : nil
|
||||
let matched = savedMatch ?? response.list.first { $0.scenicId == scenicId }
|
||||
storeItem = matched
|
||||
if let matched {
|
||||
appStore.currentStoreId = matched.id
|
||||
}
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
storeItem = nil
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 评估弹窗展示优先级:权限 > 景区 > 位置超时。
|
||||
func evaluateDialogs(api: HomeAPI) async {
|
||||
if showPermissionDialog {
|
||||
showScenicDialog = false
|
||||
return
|
||||
}
|
||||
let hasScenic = appStore.currentScenicId > 0
|
||||
showScenicDialog = !hasScenic
|
||||
if showScenicDialog || dismissedLocationTimeoutDialog {
|
||||
showLocationTimeoutDialog = false
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
guard !isMinimalTopRole else {
|
||||
showLocationTimeoutDialog = false
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
await checkLocationTimeout(api: api)
|
||||
}
|
||||
|
||||
/// 检查位置上报是否接近超时。
|
||||
func checkLocationTimeout(api: HomeAPI) async {
|
||||
guard reminderMinutes > 0 else {
|
||||
showLocationTimeoutDialog = false
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !staffId.isEmpty else {
|
||||
showLocationTimeoutDialog = true
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let detail = try await api.locationDetail(staffId: staffId)
|
||||
if detail.expireTime <= 0 {
|
||||
showLocationTimeoutDialog = true
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let currentMillis = Int64(Date().timeIntervalSince1970 * 1000)
|
||||
let expireMillis = detail.expireTime * 1000
|
||||
let threshold = expireMillis - Int64(reminderMinutes * 60 * 1000)
|
||||
showLocationTimeoutDialog = currentMillis >= threshold
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
showLocationTimeoutDialog = true
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新景区名与角色展示状态。
|
||||
func refreshLocalDisplayState() {
|
||||
currentScenicName = appStore.currentScenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
currentAppRole = appStore.currentAppRole
|
||||
isMinimalTopRole = currentAppRole?.usesHomeMinimalTopLayout ?? false
|
||||
}
|
||||
|
||||
/// 重建常用应用列表。
|
||||
func rebuildCommonMenus() {
|
||||
let permissions = appStore.permissionItems()
|
||||
let allMenus = HomeMenuCatalog.visibleMenus(from: permissions)
|
||||
commonMenus = commonMenuStore.commonMenus(
|
||||
from: allMenus,
|
||||
permissions: permissions,
|
||||
accountScope: appStore.accountCachePrefix,
|
||||
roleCode: appStore.roleCode
|
||||
)
|
||||
}
|
||||
|
||||
func hidePermissionDialog() {
|
||||
showPermissionDialog = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideScenicDialog() {
|
||||
showScenicDialog = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func showOnlineStatusSwitchDialog() {
|
||||
showOnlineStatusDialog = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideOnlineStatusSwitchDialog() {
|
||||
showOnlineStatusDialog = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideLocationTimeoutDialog() {
|
||||
showLocationTimeoutDialog = false
|
||||
dismissedLocationTimeoutDialog = true
|
||||
timeoutDialogTriggered = false
|
||||
locationStateStore.updateOnlineStatus(false)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func hideLocationReportSuccessDialog() {
|
||||
showLocationReportSuccessDialog = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func updateReminderMinutes(_ minutes: Int) {
|
||||
locationStateStore.updateReminderMinutes(minutes)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 切换在线/离线并触发位置上报。
|
||||
func switchOnlineStatus(api: HomeAPI) async {
|
||||
let now = currentTimestampMillis()
|
||||
if now - lastOnlineStatusSwitchTime < operationIntervalMillis {
|
||||
let remaining = Int((operationIntervalMillis - (now - lastOnlineStatusSwitchTime)) / 1000)
|
||||
onShowMessage?("操作过于频繁,请\(remaining)秒后再试")
|
||||
hideOnlineStatusSwitchDialog()
|
||||
return
|
||||
}
|
||||
guard appStore.currentScenicId > 0 else {
|
||||
onShowMessage?("请先选择景区")
|
||||
hideOnlineStatusSwitchDialog()
|
||||
return
|
||||
}
|
||||
|
||||
let goingOnline = !isOnline
|
||||
if goingOnline {
|
||||
do {
|
||||
_ = try await locationProvider.requestSnapshot()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
hideOnlineStatusSwitchDialog()
|
||||
return
|
||||
}
|
||||
locationStateStore.updateOnlineStatus(true)
|
||||
timeoutDialogTriggered = false
|
||||
dismissedLocationTimeoutDialog = false
|
||||
lastOnlineStatusSwitchTime = now
|
||||
onShowMessage?("已切换到在线状态,正在上报位置")
|
||||
hideOnlineStatusSwitchDialog()
|
||||
await reportLocation(api: api, type: 1)
|
||||
} else {
|
||||
locationStateStore.updateOnlineStatus(false)
|
||||
lastOnlineStatusSwitchTime = now
|
||||
onShowMessage?("已切换到离线状态")
|
||||
hideOnlineStatusSwitchDialog()
|
||||
await reportLocation(api: api, type: 3)
|
||||
}
|
||||
}
|
||||
|
||||
/// 手动上报位置。
|
||||
func manualReportLocation(api: HomeAPI) async {
|
||||
let now = currentTimestampMillis()
|
||||
if now - lastLocationReportTime < operationIntervalMillis {
|
||||
let remaining = Int((operationIntervalMillis - (now - lastLocationReportTime)) / 1000)
|
||||
onShowMessage?("操作过于频繁,请\(remaining)秒后再试")
|
||||
return
|
||||
}
|
||||
await reportLocation(api: api, type: 1)
|
||||
}
|
||||
|
||||
/// 位置超时弹窗确认上报。
|
||||
func confirmLocationTimeoutReport(api: HomeAPI) async {
|
||||
showLocationTimeoutDialog = false
|
||||
dismissedLocationTimeoutDialog = false
|
||||
await reportLocation(api: api, type: 1)
|
||||
}
|
||||
|
||||
/// 触发本地倒计时提醒。
|
||||
func triggerLocationTimeoutIfNeeded() {
|
||||
guard locationStateStore.shouldTriggerTimeoutReminder() else { return }
|
||||
guard !timeoutDialogTriggered else { return }
|
||||
timeoutDialogTriggered = true
|
||||
showLocationTimeoutDialog = true
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 刷新当前位置文案。
|
||||
func refreshLocationInfo() async {
|
||||
isLocationReporting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLocationReporting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
do {
|
||||
let snapshot = try await locationProvider.requestSnapshot()
|
||||
locationInfoText = snapshot.address.isEmpty
|
||||
? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude)
|
||||
: snapshot.address
|
||||
} catch {
|
||||
locationInfoText = "定位失败"
|
||||
}
|
||||
}
|
||||
|
||||
private func reportLocation(api: HomeAPI, type: Int) async {
|
||||
guard appStore.currentScenicId > 0 else {
|
||||
onShowMessage?("请先选择景区")
|
||||
return
|
||||
}
|
||||
let staffId = appStore.userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !staffId.isEmpty else {
|
||||
onShowMessage?("获取用户ID失败")
|
||||
return
|
||||
}
|
||||
|
||||
isLocationReporting = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isLocationReporting = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
do {
|
||||
let snapshot = try await locationProvider.requestSnapshot()
|
||||
locationInfoText = snapshot.address.isEmpty
|
||||
? String(format: "%.5f, %.5f", snapshot.latitude, snapshot.longitude)
|
||||
: snapshot.address
|
||||
_ = try await api.reportLocation(
|
||||
staffId: staffId,
|
||||
latitude: snapshot.latitude,
|
||||
longitude: snapshot.longitude,
|
||||
address: snapshot.address,
|
||||
type: type,
|
||||
scenicId: String(appStore.currentScenicId)
|
||||
)
|
||||
lastLocationReportTime = currentTimestampMillis()
|
||||
if type != 3 {
|
||||
locationStateStore.startLocationReport()
|
||||
reportTimeText = formattedNow()
|
||||
showLocationReportSuccessDialog = true
|
||||
timeoutDialogTriggered = false
|
||||
dismissedLocationTimeoutDialog = false
|
||||
}
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedNow() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
private func currentTimestampMillis() -> Int64 {
|
||||
Int64(Date().timeIntervalSince1970 * 1000)
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user