Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
637 lines
24 KiB
Swift
637 lines
24 KiB
Swift
//
|
||
// ProjectViewModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/24.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 项目表单模式,区分新建和编辑。
|
||
enum ProjectEditorMode: Equatable {
|
||
case create
|
||
case edit(PhotographerProjectDetailResponse)
|
||
}
|
||
|
||
/// 店铺项目表单模式,区分新建和编辑。
|
||
enum StoreProjectEditorMode: Equatable {
|
||
case create
|
||
case edit(PhotographerProjectDetailResponse)
|
||
}
|
||
|
||
/// 项目表单错误实体,表示校验、上传或提交失败。
|
||
enum ProjectEditorError: LocalizedError, Equatable {
|
||
case missingScenic
|
||
case missingUser
|
||
case missingName
|
||
case missingDescription
|
||
case missingPrice
|
||
case missingSpot
|
||
case missingImage
|
||
case missingStore
|
||
|
||
/// 错误文案。
|
||
var errorDescription: String? {
|
||
switch self {
|
||
case .missingScenic: return "当前缺少景区信息"
|
||
case .missingUser: return "当前缺少用户信息"
|
||
case .missingName: return "请输入项目名称"
|
||
case .missingDescription: return "请输入项目简介"
|
||
case .missingPrice: return "请填写有效价格"
|
||
case .missingSpot: return "请至少选择一个打卡点"
|
||
case .missingImage: return "请至少上传项目封面"
|
||
case .missingStore: return "请选择所属门店"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 摄影师项目管理 ViewModel,负责项目列表、分页、详情和删除。
|
||
@MainActor
|
||
final class ProjectManagementViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var items: [PhotographerProjectItem] = [] { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var loadingMore = false { didSet { onChange?() } }
|
||
private(set) var hasMore = false { didSet { onChange?() } }
|
||
private(set) var selectedDetail: PhotographerProjectDetailResponse? { didSet { onChange?() } }
|
||
var searchText = "" { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private var page = 1
|
||
private var total = 0
|
||
private let pageSize = 10
|
||
|
||
/// 重新加载摄影师项目列表。
|
||
func reload(api: any ProjectServing, scenicId: Int?) async {
|
||
guard let scenicId else {
|
||
resetList()
|
||
return
|
||
}
|
||
loading = true
|
||
errorMessage = nil
|
||
defer { loading = false }
|
||
do {
|
||
page = 1
|
||
let response = try await api.projectList(
|
||
scenicId: scenicId,
|
||
name: normalizedSearch,
|
||
page: page,
|
||
pageSize: pageSize
|
||
)
|
||
total = response.total
|
||
items = sorted(response.list)
|
||
hasMore = items.count < total
|
||
page = 2
|
||
} catch {
|
||
resetList()
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 加载下一页摄影师项目。
|
||
func loadMore(api: any ProjectServing, scenicId: Int?) async {
|
||
guard let scenicId, hasMore, !loadingMore else { return }
|
||
loadingMore = true
|
||
defer { loadingMore = false }
|
||
do {
|
||
let response = try await api.projectList(scenicId: scenicId, name: normalizedSearch, page: page, pageSize: pageSize)
|
||
total = response.total
|
||
items = sorted(deduplicated(items + response.list))
|
||
hasMore = items.count < total
|
||
page += 1
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 加载项目详情。
|
||
func loadDetail(id: Int, api: any ProjectServing) async {
|
||
do {
|
||
selectedDetail = try await api.projectDetail(id: id)
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 删除摄影师项目并从本地列表移除。
|
||
func delete(id: Int, api: any ProjectServing) async {
|
||
do {
|
||
try await api.deleteProject(id: id)
|
||
items.removeAll { $0.id == id }
|
||
total = max(total - 1, 0)
|
||
hasMore = items.count < total
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
private var normalizedSearch: String? {
|
||
let value = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return value.isEmpty ? nil : value
|
||
}
|
||
|
||
/// 重置List状态。
|
||
private func resetList() {
|
||
items = []
|
||
loading = false
|
||
loadingMore = false
|
||
hasMore = false
|
||
page = 1
|
||
total = 0
|
||
}
|
||
|
||
/// sorted 业务逻辑。
|
||
private func sorted(_ values: [PhotographerProjectItem]) -> [PhotographerProjectItem] {
|
||
values.sorted { lhs, rhs in
|
||
if lhs.status != rhs.status { return lhs.status < rhs.status }
|
||
return lhs.id > rhs.id
|
||
}
|
||
}
|
||
|
||
/// deduplicated 业务逻辑。
|
||
private func deduplicated(_ values: [PhotographerProjectItem]) -> [PhotographerProjectItem] {
|
||
var result: [Int: PhotographerProjectItem] = [:]
|
||
values.forEach { result[$0.id] = $0 }
|
||
return Array(result.values)
|
||
}
|
||
}
|
||
|
||
/// 项目编辑 ViewModel,负责摄影师项目表单、图片上传和提交。
|
||
@MainActor
|
||
final class ProjectEditorViewModel {
|
||
var onChange: (() -> Void)?
|
||
var name = "" { didSet { onChange?() } }
|
||
var descriptionText = "" { didSet { onChange?() } }
|
||
var price = "" { didSet { onChange?() } }
|
||
var otPrice = "" { didSet { onChange?() } }
|
||
var deposit = "" { didSet { onChange?() } }
|
||
var attrLabelText = "" { didSet { onChange?() } }
|
||
var materialNum = "1" { didSet { onChange?() } }
|
||
var photoNum = "1" { didSet { onChange?() } }
|
||
var videoNum = "1" { didSet { onChange?() } }
|
||
var selectedSpotIds: Set<Int> = [] { didSet { onChange?() } }
|
||
var coverImage: ProjectLocalImage? { didSet { onChange?() } }
|
||
var carouselImages: [ProjectLocalImage] = [] { didSet { onChange?() } }
|
||
var existingCoverURL = "" { didSet { onChange?() } }
|
||
var existingCarouselURLs: [String] = [] { didSet { onChange?() } }
|
||
private(set) var submitting = false { didSet { onChange?() } }
|
||
private(set) var uploadProgress = 0 { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private var mode: ProjectEditorMode
|
||
|
||
/// 初始化摄影师项目表单,并在编辑模式下回填详情。
|
||
init(mode: ProjectEditorMode) {
|
||
self.mode = mode
|
||
if case let .edit(detail) = mode {
|
||
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?,
|
||
userId: Int?,
|
||
api: any ProjectServing,
|
||
uploadService: any OSSUploadServing
|
||
) async -> Bool {
|
||
guard !submitting else { return false }
|
||
guard let scenicId else { return fail(.missingScenic) }
|
||
guard let userId, userId > 0 else { return fail(.missingUser) }
|
||
let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let normalizedDescription = descriptionText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalizedName.isEmpty else { return fail(.missingName) }
|
||
guard !normalizedDescription.isEmpty else { return fail(.missingDescription) }
|
||
guard Double(price) ?? 0 > 0 else { return fail(.missingPrice) }
|
||
guard !selectedSpotIds.isEmpty else { return fail(.missingSpot) }
|
||
|
||
submitting = true
|
||
errorMessage = nil
|
||
uploadProgress = 0
|
||
defer { submitting = false }
|
||
|
||
do {
|
||
let coverURL = try await resolveCoverURL(scenicId: scenicId, uploadService: uploadService)
|
||
guard !coverURL.isEmpty else { throw ProjectEditorError.missingImage }
|
||
let carouselURLs = try await resolveCarouselURLs(scenicId: scenicId, uploadService: uploadService)
|
||
let request = ProjectCreateRequest(
|
||
id: editID,
|
||
type: 11,
|
||
scenicId: scenicId,
|
||
name: normalizedName,
|
||
price: normalizedMoney(price) ?? "0.00",
|
||
otPrice: normalizedMoney(otPrice),
|
||
coverProject: coverURL,
|
||
coverCarousel: carouselURLs.isEmpty ? nil : carouselURLs,
|
||
description: normalizedDescription,
|
||
attrLabel: normalizedLabels,
|
||
extra: ProjectCreateExtra(
|
||
priceDeposit: normalizedMoney(deposit) ?? "0.00",
|
||
materialNum: max(Int(materialNum) ?? 0, 0),
|
||
photoNum: max(Int(photoNum) ?? 0, 0),
|
||
videoNum: max(Int(videoNum) ?? 0, 0),
|
||
scenicSpotId: selectedSpotIds.sorted(),
|
||
photogUid: [userId]
|
||
)
|
||
)
|
||
if editID == nil {
|
||
try await api.createProject(request)
|
||
} else {
|
||
try await api.editProject(request)
|
||
}
|
||
uploadProgress = 100
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
private var editID: Int? {
|
||
if case let .edit(detail) = mode {
|
||
return detail.id
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private var normalizedLabels: [String]? {
|
||
let labels = attrLabelText
|
||
.components(separatedBy: CharacterSet(charactersIn: ",,"))
|
||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||
.filter { !$0.isEmpty }
|
||
return labels.isEmpty ? nil : labels
|
||
}
|
||
|
||
/// resolveCover链接相关逻辑。
|
||
private func resolveCoverURL(scenicId: Int, uploadService: any OSSUploadServing) async throws -> String {
|
||
if let coverImage {
|
||
return try await uploadService.uploadProjectImage(data: coverImage.data, fileName: coverImage.fileName, scenicId: scenicId) { [weak self] progress in
|
||
self?.uploadProgress = progress
|
||
}
|
||
}
|
||
return existingCoverURL
|
||
}
|
||
|
||
/// resolveCarouselURLs相关逻辑。
|
||
private func resolveCarouselURLs(scenicId: Int, uploadService: any OSSUploadServing) async throws -> [String] {
|
||
var urls = existingCarouselURLs
|
||
for image in carouselImages {
|
||
let url = try await uploadService.uploadProjectImage(data: image.data, fileName: image.fileName, scenicId: scenicId) { [weak self] progress in
|
||
self?.uploadProgress = progress
|
||
}
|
||
urls.append(url)
|
||
}
|
||
return urls
|
||
}
|
||
|
||
/// 规范化dMoney格式。
|
||
private func normalizedMoney(_ value: String) -> String? {
|
||
let amount = Double(value.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0
|
||
return amount > 0 ? String(format: "%.2f", amount) : nil
|
||
}
|
||
|
||
/// 记录校验失败并返回 false。
|
||
private func fail(_ error: ProjectEditorError) -> Bool {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
/// 店铺项目管理 ViewModel,负责店铺项目列表、筛选、详情和删除。
|
||
@MainActor
|
||
final class StoreProjectManagementViewModel {
|
||
var onChange: (() -> Void)?
|
||
private(set) var items: [PhotographerProjectItem] = [] { didSet { onChange?() } }
|
||
private(set) var loading = false { didSet { onChange?() } }
|
||
private(set) var selectedDetail: PhotographerProjectDetailResponse? { didSet { onChange?() } }
|
||
var searchText = "" { didSet { onChange?() } }
|
||
var selectedType = 0 { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
/// 根据本地筛选条件返回展示项目。
|
||
var filteredItems: [PhotographerProjectItem] {
|
||
let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return items.filter { item in
|
||
let typeMatched = selectedType == 0 || item.type == selectedType
|
||
let keywordMatched = keyword.isEmpty || item.name.localizedCaseInsensitiveContains(keyword)
|
||
return typeMatched && keywordMatched
|
||
}
|
||
}
|
||
|
||
/// 重新加载店铺项目列表。
|
||
func reload(api: any ProjectServing, userId: String?) async {
|
||
let normalizedUserId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||
guard !normalizedUserId.isEmpty else {
|
||
items = []
|
||
return
|
||
}
|
||
loading = true
|
||
errorMessage = nil
|
||
defer { loading = false }
|
||
do {
|
||
let response = try await api.storeManagerProjectList(userId: normalizedUserId, page: 1, pageSize: 100)
|
||
items = response.list
|
||
} catch {
|
||
items = []
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 加载店铺项目详情。
|
||
func loadDetail(id: Int, api: any ProjectServing) async {
|
||
do {
|
||
selectedDetail = try await api.storeManagerProjectDetail(id: id)
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 删除店铺项目。
|
||
func delete(id: Int, api: any ProjectServing) async {
|
||
do {
|
||
try await api.storeManagerDeleteProject(id: id)
|
||
items.removeAll { $0.id == id }
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 店铺项目编辑 ViewModel,负责多点位和押金项目表单提交。
|
||
@MainActor
|
||
final class StoreProjectEditorViewModel {
|
||
var onChange: (() -> Void)?
|
||
var projectType = 19 { didSet { onChange?() } }
|
||
var scenicList: [StoreManagerScenicItem] = [] { didSet { onChange?() } }
|
||
var storeList: [StoreItem] = [] { didSet { onChange?() } }
|
||
var selectedScenicIds: Set<Int> = [] { didSet { onChange?() } }
|
||
var selectedStoreId: Int? { didSet { onChange?() } }
|
||
var name = "" { didSet { onChange?() } }
|
||
var descriptionText = "" { didSet { onChange?() } }
|
||
var coverImage: ProjectLocalImage? { didSet { onChange?() } }
|
||
var carouselImages: [ProjectLocalImage] = [] { didSet { onChange?() } }
|
||
var existingCoverURL = "" { didSet { onChange?() } }
|
||
var existingCarouselURLs: [String] = [] { didSet { onChange?() } }
|
||
var settleSpotNum = "1" { didSet { onChange?() } }
|
||
var projectPrice = "" { didSet { onChange?() } }
|
||
var priceMaterial = "" { didSet { onChange?() } }
|
||
var pricePhoto = "" { didSet { onChange?() } }
|
||
var priceVideo = "" { didSet { onChange?() } }
|
||
var priceDeposit = "" { didSet { onChange?() } }
|
||
var materialNum = "1" { didSet { onChange?() } }
|
||
var photoNum = "1" { didSet { onChange?() } }
|
||
var videoNum = "1" { didSet { onChange?() } }
|
||
private(set) var submitting = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
private var mode: StoreProjectEditorMode
|
||
|
||
/// 初始化店铺项目表单,并在编辑模式下回填详情。
|
||
init(mode: StoreProjectEditorMode) {
|
||
self.mode = mode
|
||
if case let .edit(detail) = mode {
|
||
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 }
|
||
do {
|
||
scenicList = try await api.storeManagerScenicList(userId: "\(userId)").list
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 加载当前景区下门店列表。
|
||
func loadStoreList(api: any ProjectServing) async {
|
||
guard projectType == 4, let scenicId = selectedScenicIds.first else { return }
|
||
do {
|
||
storeList = try await api.storeAll().list.filter { $0.scenicId == scenicId }
|
||
if selectedStoreId == nil {
|
||
selectedStoreId = storeList.first?.id
|
||
}
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
}
|
||
}
|
||
|
||
/// 切换店铺项目景区选择。
|
||
func toggleScenic(_ id: Int) {
|
||
if projectType == 4 {
|
||
selectedScenicIds = [id]
|
||
} else if selectedScenicIds.contains(id) {
|
||
selectedScenicIds.remove(id)
|
||
} else {
|
||
selectedScenicIds.insert(id)
|
||
}
|
||
}
|
||
|
||
/// 提交店铺项目。
|
||
func submit(userId: Int, api: any ProjectServing, uploadService: any OSSUploadServing) async -> Bool {
|
||
guard !submitting else { return false }
|
||
guard userId > 0 else { return fail(.missingUser) }
|
||
let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let normalizedDescription = descriptionText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !normalizedName.isEmpty else { return fail(.missingName) }
|
||
guard !normalizedDescription.isEmpty else { return fail(.missingDescription) }
|
||
guard !selectedScenicIds.isEmpty else { return fail(.missingScenic) }
|
||
guard Double(projectType == 4 ? priceDeposit : projectPrice) ?? 0 > 0 else { return fail(.missingPrice) }
|
||
if projectType == 4, selectedStoreId == nil { return fail(.missingStore) }
|
||
|
||
submitting = true
|
||
errorMessage = nil
|
||
defer { submitting = false }
|
||
|
||
do {
|
||
let scenicIdForUpload = selectedScenicIds.first ?? 0
|
||
let coverURL = try await resolveCoverURL(scenicId: scenicIdForUpload, uploadService: uploadService)
|
||
guard !coverURL.isEmpty else { throw ProjectEditorError.missingImage }
|
||
let carouselURLs = try await resolveCarouselURLs(scenicId: scenicIdForUpload, uploadService: uploadService)
|
||
if projectType == 4 {
|
||
try await submitOfflineProject(
|
||
api: api,
|
||
name: normalizedName,
|
||
description: normalizedDescription,
|
||
coverURL: coverURL,
|
||
carouselURLs: carouselURLs
|
||
)
|
||
} else {
|
||
try await submitMultiPointProject(
|
||
userId: userId,
|
||
api: api,
|
||
name: normalizedName,
|
||
description: normalizedDescription,
|
||
coverURL: coverURL,
|
||
carouselURLs: carouselURLs
|
||
)
|
||
}
|
||
return true
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|
||
|
||
private var editID: Int? {
|
||
if case let .edit(detail) = mode {
|
||
return detail.id
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// resolveCover链接相关逻辑。
|
||
private func resolveCoverURL(scenicId: Int, uploadService: any OSSUploadServing) async throws -> String {
|
||
if let coverImage {
|
||
return try await uploadService.uploadProjectImage(data: coverImage.data, fileName: coverImage.fileName, scenicId: scenicId) { _ in }
|
||
}
|
||
return existingCoverURL
|
||
}
|
||
|
||
/// resolveCarouselURLs相关逻辑。
|
||
private func resolveCarouselURLs(scenicId: Int, uploadService: any OSSUploadServing) async throws -> [String] {
|
||
var urls = existingCarouselURLs
|
||
for image in carouselImages {
|
||
let url = try await uploadService.uploadProjectImage(data: image.data, fileName: image.fileName, scenicId: scenicId) { _ in }
|
||
urls.append(url)
|
||
}
|
||
return urls
|
||
}
|
||
|
||
/// 提交MultiPointProject。
|
||
private func submitMultiPointProject(
|
||
userId: Int,
|
||
api: any ProjectServing,
|
||
name: String,
|
||
description: String,
|
||
coverURL: String,
|
||
carouselURLs: [String]
|
||
) async throws {
|
||
if let editID {
|
||
try await api.storeManagerUpdate(
|
||
StoreManagerUpdateRequest(
|
||
id: editID,
|
||
name: name,
|
||
type: 19,
|
||
description: description,
|
||
coverProject: coverURL,
|
||
coverCarousel: carouselURLs,
|
||
projectRule: nil,
|
||
scenicId: selectedScenicIds.sorted(),
|
||
settleSpotNum: max(Int(settleSpotNum) ?? 1, 1),
|
||
price: Double(projectPrice) ?? 0,
|
||
priceMaterial: Double(priceMaterial) ?? 0,
|
||
pricePhoto: Double(pricePhoto) ?? 0,
|
||
priceVideo: Double(priceVideo) ?? 0,
|
||
priceMaterialAll: nil,
|
||
packageList: nil,
|
||
userId: userId,
|
||
singleSpotMaterialNum: max(Int(materialNum) ?? 0, 0),
|
||
singleSpotPhotoNum: max(Int(photoNum) ?? 0, 0),
|
||
singleSpotVideoNum: max(Int(videoNum) ?? 0, 0)
|
||
)
|
||
)
|
||
} else {
|
||
try await api.storeManagerCreate(
|
||
StoreManagerCreateRequest(
|
||
name: name,
|
||
storeId: nil,
|
||
type: 19,
|
||
description: description,
|
||
coverProject: coverURL,
|
||
coverCarousel: carouselURLs,
|
||
projectRule: nil,
|
||
scenicId: selectedScenicIds.sorted(),
|
||
settleSpotNum: max(Int(settleSpotNum) ?? 1, 1),
|
||
price: Double(projectPrice) ?? 0,
|
||
priceMaterial: Double(priceMaterial) ?? 0,
|
||
pricePhoto: Double(pricePhoto) ?? 0,
|
||
priceVideo: Double(priceVideo) ?? 0,
|
||
priceMaterialAll: nil,
|
||
packageList: nil,
|
||
userId: userId,
|
||
singleSpotMaterialNum: max(Int(materialNum) ?? 0, 0),
|
||
singleSpotPhotoNum: max(Int(photoNum) ?? 0, 0),
|
||
singleSpotVideoNum: max(Int(videoNum) ?? 0, 0)
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 提交OfflineProject。
|
||
private func submitOfflineProject(api: any ProjectServing, name: String, description: String, coverURL: String, carouselURLs: [String]) async throws {
|
||
let scenicId = selectedScenicIds.first ?? 0
|
||
let storeId = selectedStoreId ?? 0
|
||
if let editID {
|
||
try await api.storeManagerOfflineUpdate(
|
||
StoreManagerOfflineUpdateRequest(
|
||
id: editID,
|
||
name: name,
|
||
scenicId: scenicId,
|
||
description: description,
|
||
price: Double(priceDeposit) ?? 0,
|
||
coverProject: coverURL,
|
||
coverCarousel: carouselURLs,
|
||
storeId: storeId
|
||
)
|
||
)
|
||
} else {
|
||
try await api.storeManagerOfflineCreate(
|
||
StoreManagerOfflineCreateRequest(
|
||
name: name,
|
||
scenicId: scenicId,
|
||
storeId: storeId,
|
||
description: description,
|
||
price: Double(priceDeposit) ?? 0,
|
||
coverProject: coverURL,
|
||
coverCarousel: carouselURLs
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 记录校验失败并返回 false。
|
||
private func fail(_ error: ProjectEditorError) -> Bool {
|
||
errorMessage = error.localizedDescription
|
||
return false
|
||
}
|
||
}
|