从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user