将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。 Co-authored-by: Cursor <cursoragent@cursor.com>
290 lines
9.9 KiB
Swift
290 lines
9.9 KiB
Swift
//
|
|
// 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: "请先选择店铺"
|
|
}
|
|
}
|
|
}
|