从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构

将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 10:16:35 +08:00
parent 0314033a7f
commit 703078352c
127 changed files with 2320 additions and 1465 deletions

View File

@ -6,7 +6,7 @@
//
import Foundation
import Observation
import Combine
///
enum ProjectEditorMode: Equatable {
@ -48,18 +48,17 @@ enum ProjectEditorError: LocalizedError, Equatable {
/// ViewModel
@MainActor
@Observable
final class ProjectManagementViewModel {
private(set) var items: [PhotographerProjectItem] = []
private(set) var loading = false
private(set) var loadingMore = false
private(set) var hasMore = false
private(set) var selectedDetail: PhotographerProjectDetailResponse?
var searchText = ""
var errorMessage: String?
final class ProjectManagementViewModel: ObservableObject {
@Published private(set) var items: [PhotographerProjectItem] = []
@Published private(set) var loading = false
@Published private(set) var loadingMore = false
@Published private(set) var hasMore = false
@Published private(set) var selectedDetail: PhotographerProjectDetailResponse?
@Published var searchText = ""
@Published var errorMessage: String?
private var page = 1
private var total = 0
@Published private var page = 1
@Published private var total = 0
private let pageSize = 10
///
@ -156,47 +155,54 @@ final class ProjectManagementViewModel {
/// ViewModel
@MainActor
@Observable
final class ProjectEditorViewModel {
var name = ""
var descriptionText = ""
var price = ""
var otPrice = ""
var deposit = ""
var attrLabelText = ""
var materialNum = "1"
var photoNum = "1"
var videoNum = "1"
var selectedSpotIds: Set<Int> = []
var coverImage: ProjectLocalImage?
var carouselImages: [ProjectLocalImage] = []
var existingCoverURL = ""
var existingCarouselURLs: [String] = []
private(set) var submitting = false
private(set) var uploadProgress = 0
var errorMessage: String?
final class ProjectEditorViewModel: ObservableObject {
@Published var name = ""
@Published var descriptionText = ""
@Published var price = ""
@Published var otPrice = ""
@Published var deposit = ""
@Published var attrLabelText = ""
@Published var materialNum = "1"
@Published var photoNum = "1"
@Published var videoNum = "1"
@Published var selectedSpotIds: Set<Int> = []
@Published var coverImage: ProjectLocalImage?
@Published var carouselImages: [ProjectLocalImage] = []
@Published var existingCoverURL = ""
@Published var existingCarouselURLs: [String] = []
@Published private(set) var submitting = false
@Published private(set) var uploadProgress = 0
@Published var errorMessage: String?
private let mode: ProjectEditorMode
private var mode: ProjectEditorMode
///
init(mode: ProjectEditorMode) {
self.mode = mode
if case let .edit(detail) = mode {
name = detail.name
descriptionText = detail.description
price = detail.price
otPrice = detail.otPrice
deposit = detail.priceDeposit
attrLabelText = detail.attrLabel.joined(separator: "")
materialNum = "\(max(detail.materialNum, 0))"
photoNum = "\(max(detail.photoNum, 0))"
videoNum = "\(max(detail.videoNum, 0))"
selectedSpotIds = Set(detail.scenicList.map(\.id))
existingCoverURL = detail.coverProject
existingCarouselURLs = detail.coverCarousel
apply(detail)
}
}
/// ObservableObject
func apply(_ detail: PhotographerProjectDetailResponse) {
mode = .edit(detail)
name = detail.name
descriptionText = detail.description
price = detail.price
otPrice = detail.otPrice
deposit = detail.priceDeposit
attrLabelText = detail.attrLabel.joined(separator: "")
materialNum = "\(max(detail.materialNum, 0))"
photoNum = "\(max(detail.photoNum, 0))"
videoNum = "\(max(detail.videoNum, 0))"
selectedSpotIds = Set(detail.scenicList.map(\.id))
existingCoverURL = detail.coverProject
existingCarouselURLs = detail.coverCarousel
coverImage = nil
carouselImages = []
}
///
func submit(
scenicId: Int?,
@ -304,14 +310,13 @@ final class ProjectEditorViewModel {
/// ViewModel
@MainActor
@Observable
final class StoreProjectManagementViewModel {
private(set) var items: [PhotographerProjectItem] = []
private(set) var loading = false
private(set) var selectedDetail: PhotographerProjectDetailResponse?
var searchText = ""
var selectedType = 0
var errorMessage: String?
final class StoreProjectManagementViewModel: ObservableObject {
@Published private(set) var items: [PhotographerProjectItem] = []
@Published private(set) var loading = false
@Published private(set) var selectedDetail: PhotographerProjectDetailResponse?
@Published var searchText = ""
@Published var selectedType = 0
@Published var errorMessage: String?
///
var filteredItems: [PhotographerProjectItem] {
@ -364,51 +369,58 @@ final class StoreProjectManagementViewModel {
/// ViewModel
@MainActor
@Observable
final class StoreProjectEditorViewModel {
var projectType = 19
var scenicList: [StoreManagerScenicItem] = []
var storeList: [StoreItem] = []
var selectedScenicIds: Set<Int> = []
var selectedStoreId: Int?
var name = ""
var descriptionText = ""
var coverImage: ProjectLocalImage?
var carouselImages: [ProjectLocalImage] = []
var existingCoverURL = ""
var existingCarouselURLs: [String] = []
var settleSpotNum = "1"
var projectPrice = ""
var priceMaterial = ""
var pricePhoto = ""
var priceVideo = ""
var priceDeposit = ""
var materialNum = "1"
var photoNum = "1"
var videoNum = "1"
private(set) var submitting = false
var errorMessage: String?
final class StoreProjectEditorViewModel: ObservableObject {
@Published var projectType = 19
@Published var scenicList: [StoreManagerScenicItem] = []
@Published var storeList: [StoreItem] = []
@Published var selectedScenicIds: Set<Int> = []
@Published var selectedStoreId: Int?
@Published var name = ""
@Published var descriptionText = ""
@Published var coverImage: ProjectLocalImage?
@Published var carouselImages: [ProjectLocalImage] = []
@Published var existingCoverURL = ""
@Published var existingCarouselURLs: [String] = []
@Published var settleSpotNum = "1"
@Published var projectPrice = ""
@Published var priceMaterial = ""
@Published var pricePhoto = ""
@Published var priceVideo = ""
@Published var priceDeposit = ""
@Published var materialNum = "1"
@Published var photoNum = "1"
@Published var videoNum = "1"
@Published private(set) var submitting = false
@Published var errorMessage: String?
private let mode: StoreProjectEditorMode
private var mode: StoreProjectEditorMode
///
init(mode: StoreProjectEditorMode) {
self.mode = mode
if case let .edit(detail) = mode {
projectType = detail.type == 4 ? 4 : 19
name = detail.name
descriptionText = detail.description
existingCoverURL = detail.coverProject
existingCarouselURLs = detail.coverCarousel
selectedScenicIds = Set(detail.scenicList.map(\.id))
projectPrice = detail.price
priceDeposit = detail.priceDeposit
materialNum = "\(max(detail.materialNum, 0))"
photoNum = "\(max(detail.photoNum, 0))"
videoNum = "\(max(detail.videoNum, 0))"
apply(detail)
}
}
/// ObservableObject
func apply(_ detail: PhotographerProjectDetailResponse) {
mode = .edit(detail)
projectType = detail.type == 4 ? 4 : 19
name = detail.name
descriptionText = detail.description
existingCoverURL = detail.coverProject
existingCarouselURLs = detail.coverCarousel
selectedScenicIds = Set(detail.scenicList.map(\.id))
projectPrice = detail.price
priceDeposit = detail.priceDeposit
materialNum = "\(max(detail.materialNum, 0))"
photoNum = "\(max(detail.photoNum, 0))"
videoNum = "\(max(detail.videoNum, 0))"
coverImage = nil
carouselImages = []
}
///
func loadScenicList(api: any ProjectServing, userId: Int) async {
guard scenicList.isEmpty, userId > 0 else { return }