Files
suixinkan_ios_new/suixinkan/Features/ScenicPromotion/ViewModels/ScenicPromotionViewModels.swift

278 lines
11 KiB
Swift
Raw 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.

//
// ScenicPromotionViewModels.swift
// suixinkan
//
// Created by Codex on 2026/7/2.
//
import Combine
import Foundation
import SwiftUI
/// ViewModel
@MainActor
final class ScenicPromotionViewModel: ObservableObject {
@Published private(set) var pageState: ScenicPromotionPageState = .idle
@Published private(set) var board: ScenicPromotionBoard?
@Published var selectedSpotID: String?
@Published var selectedMode: ScenicPromotionMediaMode = .video
@Published var selectedMediaID: ScenicPromotionMedia.ID?
@Published private(set) var activeScenario: ScenicPromotionDemoScenario = .normal
/// selectedSpotID
var selectedSpot: ScenicPromotionSpot? {
guard let board else { return nil }
return board.spots.first { $0.id == selectedSpotID } ?? board.spots.first
}
/// nil
var queue: ScenicPromotionQueueInfo? {
guard activeScenario != .noQueue else { return nil }
return selectedSpot?.queue
}
/// /
var mediaItems: [ScenicPromotionMedia] {
guard activeScenario != .noMedia else { return [] }
return Self.mediaItems(for: selectedSpot, mode: selectedMode)
}
/// ID
var selectedMedia: ScenicPromotionMedia? {
let items = mediaItems
return items.first { $0.id == selectedMediaID } ?? items.first
}
var hasQueueData: Bool {
queue != nil
}
/// Mock
func loadIfNeeded() async {
guard pageState == .idle else { return }
await load()
}
/// 便 Loading
func load() async {
withAnimation(.easeInOut(duration: 0.25)) {
pageState = .loading
}
try? await Task.sleep(nanoseconds: 350_000_000)
guard !Task.isCancelled else { return }
applyScenario(.normal)
}
///
func applyScenario(_ scenario: ScenicPromotionDemoScenario) {
activeScenario = scenario
withAnimation(.easeInOut(duration: 0.28)) {
switch scenario {
case .normal, .noMedia, .noQueue:
board = ScenicPromotionMockStore.mockBoard
selectedSpotID = selectedSpotID ?? board?.spots.first?.id
selectedMediaID = Self.mediaItems(for: selectedSpot, mode: selectedMode).first?.id
pageState = .normal
case .loading:
pageState = .loading
case .error:
board = nil
pageState = .error("宣传页数据加载失败,请稍后重试")
case .empty:
board = nil
pageState = .empty
}
}
}
///
func selectSpot(_ spot: ScenicPromotionSpot) {
withAnimation(.spring(response: 0.28, dampingFraction: 0.82)) {
selectedSpotID = spot.id
selectedMediaID = Self.mediaItems(for: spot, mode: selectedMode).first?.id
}
}
/// /
func selectMode(_ mode: ScenicPromotionMediaMode) {
withAnimation(.spring(response: 0.28, dampingFraction: 0.82)) {
selectedMode = mode
selectedMediaID = Self.mediaItems(for: selectedSpot, mode: mode).first?.id
}
}
///
func selectMedia(_ media: ScenicPromotionMedia) {
withAnimation(.spring(response: 0.28, dampingFraction: 0.82)) {
selectedMediaID = media.id
}
}
///
func moveMedia(_ offset: Int) {
let items = mediaItems
guard !items.isEmpty else { return }
let currentIndex = items.firstIndex { $0.id == selectedMedia?.id } ?? 0
let nextIndex = (currentIndex + offset + items.count) % items.count
selectMedia(items[nextIndex])
}
///
static func mediaItems(
for spot: ScenicPromotionSpot?,
mode: ScenicPromotionMediaMode
) -> [ScenicPromotionMedia] {
switch mode {
case .video:
return spot?.videos ?? []
case .photo:
return spot?.photos ?? []
}
}
}
/// ViewModel
@MainActor
final class ScenicPromotionSettingsViewModel: ObservableObject {
@Published private(set) var pageState: ScenicPromotionSettingsPageState = .idle
@Published private(set) var queuePoints: [ScenicPromotionQueuePoint] = []
@Published private(set) var materialPoints: [ScenicPromotionMaterialPoint] = []
@Published var selectedQueuePointIDs: Set<String> = []
@Published var selectedMaterialPointID: String?
@Published private(set) var materialsByPointID: [String: [ScenicPromotionManagedMaterial]] = [:]
///
@Published var previewMaterial: ScenicPromotionManagedMaterial?
///
var selectedQueuePointNames: String {
let names = queuePoints
.filter { selectedQueuePointIDs.contains($0.id) }
.map(\.name)
return names.isEmpty ? "请选择排队打卡点" : names.joined(separator: "")
}
///
var selectedMaterialPoint: ScenicPromotionMaterialPoint? {
guard let selectedMaterialPointID else { return nil }
return materialPoints.first { $0.id == selectedMaterialPointID }
}
///
var selectedMaterialPointName: String {
selectedMaterialPoint?.name ?? "请选择素材所属打卡点"
}
/// Mock
func loadIfNeeded() async {
guard pageState == .idle else { return }
await load()
}
///
func load() async {
withAnimation(.easeInOut(duration: 0.24)) {
pageState = .loading
}
try? await Task.sleep(nanoseconds: 260_000_000)
guard !Task.isCancelled else { return }
applyScenario(.normal)
}
///
func applyScenario(_ scenario: ScenicPromotionSettingsDemoScenario) {
withAnimation(.easeInOut(duration: 0.24)) {
switch scenario {
case .normal:
queuePoints = ScenicPromotionMockStore.mockQueuePoints
materialPoints = ScenicPromotionMockStore.mockMaterialPoints
selectedQueuePointIDs = ["queue_bridge", "queue_hendi"]
selectedMaterialPointID = selectedMaterialPointID ?? ScenicPromotionMockStore.mockMaterialPoints.first?.id
materialsByPointID = ScenicPromotionMockStore.mockMaterialsByPointID
pageState = .normal
case .loading:
pageState = .loading
case .empty:
queuePoints = []
materialPoints = []
selectedQueuePointIDs = []
selectedMaterialPointID = nil
materialsByPointID = [:]
pageState = .empty
case .error:
queuePoints = []
materialPoints = []
selectedQueuePointIDs = []
selectedMaterialPointID = nil
materialsByPointID = [:]
pageState = .error("宣传页设置数据加载失败,请稍后重试")
}
}
}
///
func toggleQueuePoint(_ point: ScenicPromotionQueuePoint) {
withAnimation(.spring(response: 0.25, dampingFraction: 0.86)) {
if selectedQueuePointIDs.contains(point.id) {
selectedQueuePointIDs.remove(point.id)
} else {
selectedQueuePointIDs.insert(point.id)
}
}
}
///
func selectMaterialPoint(_ point: ScenicPromotionMaterialPoint) {
withAnimation(.spring(response: 0.25, dampingFraction: 0.86)) {
selectedMaterialPointID = point.id
}
}
///
func materials(for kind: ScenicPromotionUploadKind) -> [ScenicPromotionManagedMaterial] {
guard let selectedMaterialPointID else { return [] }
return (materialsByPointID[selectedMaterialPointID] ?? [])
.filter { $0.kind == kind }
}
/// false
func addMaterial(kind: ScenicPromotionUploadKind) -> Bool {
guard let point = selectedMaterialPoint else { return false }
let currentItems = materialsByPointID[point.id] ?? []
let nextIndex = currentItems.filter { $0.kind == kind }.count + 1
let assetName = ScenicPromotionMockStore.assetName(for: kind, index: nextIndex)
let material = ScenicPromotionManagedMaterial(
id: "\(point.id)_\(kind.rawValue)_\(UUID().uuidString)",
pointID: point.id,
kind: kind,
title: "\(point.name)\(kind.shortTitle)\(nextIndex)",
subtitle: kind == .video ? "模拟上传视频,待后续接入真实素材" : "模拟上传图片,待后续接入真实素材",
assetName: assetName,
duration: kind == .video ? "00:\(String(format: "%02d", 20 + nextIndex * 3))" : nil,
createdAt: "2026-06-25 19:\(String(format: "%02d", 20 + nextIndex))"
)
withAnimation(.spring(response: 0.28, dampingFraction: 0.86)) {
materialsByPointID[point.id, default: []].append(material)
}
return true
}
///
func deleteMaterial(_ material: ScenicPromotionManagedMaterial) {
withAnimation(.spring(response: 0.25, dampingFraction: 0.86)) {
materialsByPointID[material.pointID]?.removeAll { $0.id == material.id }
}
}
///
func openMaterialPreview(_ material: ScenicPromotionManagedMaterial) {
previewMaterial = material
}
///
func submit() -> Bool {
guard !selectedQueuePointIDs.isEmpty else { return false }
return true
}
}