Files
suixinkan_ios_new/suixinkan/Features/PunchPoint/ViewModels/PunchPointViewModels.swift
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

286 lines
9.6 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.

//
// PunchPointViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import Foundation
import Combine
/// ViewModel
@MainActor
final class PunchPointListViewModel: ObservableObject {
@Published var selectedFilter: PunchPointFilter = .all
@Published var items: [PunchPointItem] = []
@Published var selectedDetail: PunchPointItem?
@Published var errorMessage: String?
@Published var isLoading = false
@Published var isLoadingMore = false
@Published var total = 0
@Published private var page = 1
private let pageSize = 20
///
var hasMore: Bool {
items.count < total
}
///
func reload(scenicId: Int?, api: any PunchPointServing) async {
guard let scenicId else {
reset()
return
}
page = 1
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let payload = try await api.punchPointList(
scenicId: scenicId,
status: selectedFilter.rawValue,
page: page,
pageSize: pageSize
)
items = payload.list
total = payload.total
} catch {
items = []
total = 0
errorMessage = error.localizedDescription
}
}
///
func loadMore(scenicId: Int?, api: any PunchPointServing) async {
guard let scenicId, hasMore, !isLoadingMore, !isLoading else { return }
isLoadingMore = true
let nextPage = page + 1
defer { isLoadingMore = false }
do {
let payload = try await api.punchPointList(
scenicId: scenicId,
status: selectedFilter.rawValue,
page: nextPage,
pageSize: pageSize
)
page = nextPage
items.append(contentsOf: payload.list)
total = payload.total
} catch {
errorMessage = error.localizedDescription
}
}
///
func selectFilter(_ filter: PunchPointFilter, scenicId: Int?, api: any PunchPointServing) async {
guard selectedFilter != filter else { return }
selectedFilter = filter
await reload(scenicId: scenicId, api: api)
}
///
func loadDetail(id: Int, api: any PunchPointServing) async {
errorMessage = nil
do {
selectedDetail = try await api.punchPointInfo(id: id)
} catch {
selectedDetail = items.first { $0.id == id }
errorMessage = error.localizedDescription
}
}
///
func delete(_ item: PunchPointItem, scenicId: Int?, api: any PunchPointServing) async -> Bool {
do {
try await api.deletePunchPoint(id: item.id)
await reload(scenicId: scenicId, api: api)
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
func reset() {
items = []
selectedDetail = nil
errorMessage = nil
total = 0
page = 1
isLoading = false
isLoadingMore = false
}
}
/// ViewModel OSS
@MainActor
final class PunchPointEditorViewModel: ObservableObject {
@Published var name = ""
@Published var description = ""
@Published var address = ""
@Published var latitudeText = ""
@Published var longitudeText = ""
@Published var scenicSpotText = ""
@Published var remoteImages: [String] = []
@Published var localImages: [PunchPointLocalImage] = []
@Published var errorMessage: String?
@Published var isSubmitting = false
private(set) var editingItem: PunchPointItem?
/// ViewModel
init(item: PunchPointItem? = nil) {
editingItem = item
if let item {
apply(item)
}
}
/// ObservableObject
func apply(_ item: PunchPointItem) {
editingItem = item
name = item.name
description = item.description
address = item.region?.address ?? ""
latitudeText = item.region.map { String($0.lat) } ?? ""
longitudeText = item.region.map { String($0.lot) } ?? ""
scenicSpotText = item.scenicSpotStr
remoteImages = item.guideImages
localImages = []
}
///
func applyLocation(latitude: Double, longitude: Double, address: String) {
latitudeText = String(format: "%.6f", latitude)
longitudeText = String(format: "%.6f", longitude)
self.address = address
if scenicSpotText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
scenicSpotText = address
}
}
///
func addLocalImages(_ images: [PunchPointLocalImage]) {
localImages.append(contentsOf: images)
}
///
func removeRemoteImage(_ url: String) {
remoteImages.removeAll { $0 == url }
}
///
func removeLocalImage(id: UUID) {
localImages.removeAll { $0.id == id }
}
/// OSS
func submit(
scenicId: Int?,
api: any PunchPointServing,
uploadService: any OSSUploadServing
) async -> Bool {
guard !isSubmitting else { return false }
guard let scenicId else {
errorMessage = "缺少当前景区"
return false
}
guard let region = makeRegion() else { return false }
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty else {
errorMessage = "请输入打卡点名称"
return false
}
guard !region.address.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
errorMessage = "请选择或填写打卡点地址"
return false
}
guard !remoteImages.isEmpty || !localImages.isEmpty else {
errorMessage = "请至少上传一张打卡点图片"
return false
}
isSubmitting = true
errorMessage = nil
defer { isSubmitting = false }
do {
let uploadedImages = try await uploadImages(scenicId: scenicId, uploadService: uploadService)
let allImages = remoteImages + uploadedImages
if let editingItem {
try await api.editPunchPoint(
EditPunchPointRequest(
id: editingItem.id,
scenicAreaId: "\(scenicId)",
name: trimmedName,
description: description.trimmingCharacters(in: .whitespacesAndNewlines),
region: region,
scenicSpotStr: scenicSpotText.trimmingCharacters(in: .whitespacesAndNewlines),
guideImages: allImages
)
)
} else {
try await api.addPunchPoint(
AddPunchPointRequest(
scenicAreaId: "\(scenicId)",
name: trimmedName,
description: description.trimmingCharacters(in: .whitespacesAndNewlines),
region: region,
scenicSpotStr: scenicSpotText.trimmingCharacters(in: .whitespacesAndNewlines),
guideImages: allImages
)
)
}
remoteImages = allImages
localImages = []
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
///
private func makeRegion() -> PunchPointRegion? {
let trimmedAddress = address.trimmingCharacters(in: .whitespacesAndNewlines)
guard let lat = Double(latitudeText.trimmingCharacters(in: .whitespacesAndNewlines)),
let lot = Double(longitudeText.trimmingCharacters(in: .whitespacesAndNewlines)) else {
errorMessage = "请选择打卡点坐标"
return nil
}
return PunchPointRegion(
lat: lat,
lot: lot,
address: trimmedAddress,
scenicSpotStr: scenicSpotText.trimmingCharacters(in: .whitespacesAndNewlines)
)
}
/// OSS URL
private func uploadImages(scenicId: Int, uploadService: any OSSUploadServing) async throws -> [String] {
var uploaded: [String] = []
for index in localImages.indices {
let local = localImages[index]
let url = try await uploadService.uploadPunchPointImage(
data: local.data,
fileName: local.fileName,
scenicId: scenicId
) { [weak self] progress in
Task { @MainActor in
self?.localImages[index].progress = progress
}
}
localImages[index].remoteURL = url
uploaded.append(url)
}
return uploaded
}
}