// // 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.currentScenicId, range: range) default: return try await api.photographerSummary(scenicId: AppStore.shared.currentScenicId, range: range) } } private func fetchDailyList(api: StatisticsAPI) async throws -> ListDataResponse { 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.currentScenicId, startTime: startTime, endTime: endTime, page: currentPage ) default: return try await api.photographerDaily( scenicId: AppStore.shared.currentScenicId, startTime: startTime, endTime: endTime, page: currentPage ) } } private var currentRole: AppRoleCode { AppStore.shared.currentAppRole ?? .photographer } private func resolveStoreId() throws -> Int { let storeId = AppStore.shared.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: "请先选择店铺" } } }