Files
suixinkan_uikit/suixinkan/Features/Statistics/ViewModels/StatisticsViewModel.swift
汉秋 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

290 lines
9.9 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// StatisticsViewModel.swift
// suixinkan
//
import Foundation
/// Tab ViewModel Android `StatisticsViewModel`
final class StatisticsViewModel {
private(set) var statistics: StatisticsSummaryResponse?
private(set) var selectedPeriod = StatisticsSummaryPeriod.today.rawValue
private(set) var selectedPeriodTime = ""
private(set) var statisticsList: [StatisticsDailyItem] = []
private(set) var statisticsRangeText = "今日"
private(set) var rangeStartDate: Date?
private(set) var rangeEndDate: Date?
private(set) var isRefreshing = false
private(set) var canLoadMore = false
private(set) var isLoadingSummary = false
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private var currentPage = 1
private var totalCount = 0
private var startTime = ""
private var endTime = ""
private let apiDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
private let uiDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy/MM/dd"
return formatter
}()
private var rangeMap: [String: String] {
Dictionary(uniqueKeysWithValues: StatisticsSummaryPeriod.allCases.map { ($0.rawValue, $0.rangeValue) })
}
/// Tab
func initStatistics(api: StatisticsAPI) async {
await loadStatistics(StatisticsSummaryPeriod.today.rawValue, api: api)
await updateRangeByShortcut(StatisticsRangeShortcut.today.rawValue, api: api)
}
/// chip
func selectPeriod(_ period: String, api: StatisticsAPI) async {
await loadStatistics(period, api: api)
}
///
func loadStatistics(_ period: String, api: StatisticsAPI) async {
isLoadingSummary = true
selectedPeriod = period
updateSelectedPeriodTime(period)
notifyStateChange()
let range = rangeMap[period] ?? StatisticsSummaryPeriod.today.rangeValue
defer {
isLoadingSummary = false
notifyStateChange()
}
do {
statistics = try await fetchSummary(range: range, api: api)
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
///
func refreshStatistics(api: StatisticsAPI) async {
currentPage = 1
isRefreshing = true
notifyStateChange()
await fetchStatisticsList(api: api)
}
///
func loadMore(api: StatisticsAPI) async {
guard statisticsList.count < totalCount else { return }
currentPage += 1
await fetchStatisticsList(api: api)
}
///
func updateRangeByShortcut(_ shortcut: String, api: StatisticsAPI) async {
let today = startOfToday()
switch shortcut {
case StatisticsRangeShortcut.today.rawValue:
await applyRange(start: today, end: today, label: StatisticsRangeShortcut.today.rawValue, api: api)
case StatisticsRangeShortcut.yesterday.rawValue:
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
await applyRange(start: yesterday, end: yesterday, label: StatisticsRangeShortcut.yesterday.rawValue, api: api)
case StatisticsRangeShortcut.last7Days.rawValue:
let start = calendar.date(byAdding: .day, value: -6, to: today) ?? today
await applyRange(start: start, end: today, label: StatisticsRangeShortcut.last7Days.rawValue, api: api)
case StatisticsRangeShortcut.all.rawValue:
startTime = ""
endTime = ""
rangeStartDate = nil
rangeEndDate = nil
statisticsRangeText = StatisticsRangeShortcut.all.rawValue
notifyStateChange()
await refreshStatistics(api: api)
default:
break
}
}
///
func updateRangeByCalendar(start: Date, end: Date, api: StatisticsAPI) async {
await applyRange(start: startOfDay(start), end: startOfDay(end), label: nil, api: api)
}
///
var listStartTime: String { startTime }
///
var listEndTime: String { endTime }
///
var listCurrentPage: Int { currentPage }
/// range
func rangeValue(for period: String) -> String {
rangeMap[period] ?? StatisticsSummaryPeriod.today.rangeValue
}
///
func formattedSelectedPeriodTime(for period: String) -> String {
let previous = selectedPeriod
selectedPeriod = period
updateSelectedPeriodTime(period)
let result = selectedPeriodTime
selectedPeriod = previous
return result
}
private func fetchStatisticsList(api: StatisticsAPI) async {
defer {
isRefreshing = false
notifyStateChange()
}
do {
let response = try await fetchDailyList(api: api)
totalCount = response.total
if currentPage == 1 {
statisticsList = response.data
} else {
statisticsList.append(contentsOf: response.data)
}
canLoadMore = statisticsList.count < totalCount
notifyStateChange()
} catch is CancellationError {
return
} catch {
onShowMessage?(error.localizedDescription)
}
}
private func applyRange(start: Date, end: Date, label: String?, api: StatisticsAPI) async {
startTime = apiDateFormatter.string(from: start)
endTime = apiDateFormatter.string(from: end)
rangeStartDate = start
rangeEndDate = end
statisticsRangeText = label ?? "\(uiDateFormatter.string(from: start))\(uiDateFormatter.string(from: end))"
notifyStateChange()
await refreshStatistics(api: api)
}
private func updateSelectedPeriodTime(_ period: String) {
let today = startOfToday()
let (start, end): (Date, Date)
switch period {
case StatisticsSummaryPeriod.today.rawValue:
(start, end) = (today, today)
case StatisticsSummaryPeriod.yesterday.rawValue:
let yesterday = calendar.date(byAdding: .day, value: -1, to: today) ?? today
(start, end) = (yesterday, yesterday)
case StatisticsSummaryPeriod.last7Days.rawValue:
let startDate = calendar.date(byAdding: .day, value: -6, to: today) ?? today
(start, end) = (startDate, today)
case StatisticsSummaryPeriod.thisMonth.rawValue:
let components = calendar.dateComponents([.year, .month], from: today)
let firstDay = calendar.date(from: components) ?? today
(start, end) = (firstDay, today)
default:
(start, end) = (today, today)
}
selectedPeriodTime = "\(uiDateFormatter.string(from: start))\(uiDateFormatter.string(from: end))"
}
private func fetchSummary(range: String, api: StatisticsAPI) async throws -> StatisticsSummaryResponse {
switch currentRole {
case .storeAdmin:
let storeId = try resolveStoreId()
return try await api.storeSummary(storeId: storeId, range: range)
case .scenicAdmin:
return try await api.scenicAdminSummary(scenicId: AppStore.shared.session.currentScenicId, range: range)
default:
return try await api.photographerSummary(scenicId: AppStore.shared.session.currentScenicId, range: range)
}
}
private func fetchDailyList(api: StatisticsAPI) async throws -> ListDataResponse<StatisticsDailyItem> {
switch currentRole {
case .storeAdmin:
let storeId = try resolveStoreId()
return try await api.storeDaily(
storeId: storeId,
startTime: startTime,
endTime: endTime,
page: currentPage
)
case .scenicAdmin:
return try await api.scenicAdminDaily(
scenicId: AppStore.shared.session.currentScenicId,
startTime: startTime,
endTime: endTime,
page: currentPage
)
default:
return try await api.photographerDaily(
scenicId: AppStore.shared.session.currentScenicId,
startTime: startTime,
endTime: endTime,
page: currentPage
)
}
}
private var currentRole: AppRoleCode {
AppStore.shared.session.currentAppRole ?? .photographer
}
private func resolveStoreId() throws -> Int {
let storeId = AppStore.shared.session.currentStoreId
guard storeId > 0 else {
throw StatisticsFlowError.missingStore
}
return storeId
}
private var calendar: Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "zh_CN")
calendar.timeZone = TimeZone.current
return calendar
}
private func startOfToday() -> Date {
startOfDay(Date())
}
private func startOfDay(_ date: Date) -> Date {
calendar.startOfDay(for: date)
}
private func notifyStateChange() {
onStateChange?()
}
}
enum StatisticsFlowError: LocalizedError {
case missingStore
var errorDescription: String? {
switch self {
case .missingStore: "请先选择店铺"
}
}
}