为旅拍 OTG 上传增加 client_photo_id,并按服务端已登记 ID 同步本地上传状态。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -32,6 +32,9 @@ protocol TravelAlbumServing {
|
||||
/// 上传并登记旅拍相册素材。
|
||||
func uploadMaterial(_ request: TravelAlbumUploadMaterialRequest) async throws -> TravelAlbumMaterial
|
||||
|
||||
/// 拉取相册内已登记素材的客户端照片 ID。
|
||||
func materialClientPhotoIds(userEquityTravelId: Int) async throws -> TravelAlbumMaterialClientPhotoIDsResponse
|
||||
|
||||
/// 删除旅拍相册。
|
||||
func deleteAlbum(id: Int) async throws
|
||||
|
||||
@ -115,6 +118,19 @@ final class TravelAlbumAPI: TravelAlbumServing {
|
||||
try await client.send(APIRequest(method: .post, path: "\(basePath)/upload-material", body: request))
|
||||
}
|
||||
|
||||
/// 拉取相册内已登记素材的客户端照片 ID。
|
||||
func materialClientPhotoIds(userEquityTravelId: Int) async throws -> TravelAlbumMaterialClientPhotoIDsResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "\(basePath)/material-client-photo-ids",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "user_equity_travel_id", value: String(userEquityTravelId)),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 删除旅拍相册。
|
||||
func deleteAlbum(id: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
|
||||
@ -203,11 +203,22 @@ struct TravelAlbumUploadMaterialRequest: Encodable, Sendable, Equatable {
|
||||
let userEquityTravelId: Int
|
||||
let fileName: String
|
||||
let fileUrl: String
|
||||
let clientPhotoId: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case userEquityTravelId = "user_equity_travel_id"
|
||||
case fileName = "file_name"
|
||||
case fileUrl = "file_url"
|
||||
case clientPhotoId = "client_photo_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 旅拍相册服务端已登记的客户端照片 ID 集合。
|
||||
struct TravelAlbumMaterialClientPhotoIDsResponse: Decodable, Sendable, Equatable {
|
||||
let clientPhotoIds: [String]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientPhotoIds = "client_photo_ids"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 生成并规范化旅拍照片的客户端唯一标识。
|
||||
enum TravelAlbumClientPhotoID {
|
||||
/// 生成小写、带连字符的 UUID v4 字符串。
|
||||
static func make() -> String {
|
||||
UUID().uuidString.lowercased()
|
||||
}
|
||||
|
||||
/// 规范化服务端或本地保存的客户端照片 ID。
|
||||
static func normalize(_ value: String?) -> String? {
|
||||
guard let normalized = value?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased(),
|
||||
!normalized.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
/// OTG 传输照片上传状态,对齐 Android `WiredTransferUploadStatus`。
|
||||
enum TravelAlbumOTGUploadStatus: String, Codable, Equatable, Sendable {
|
||||
case pending = "PENDING"
|
||||
@ -25,6 +44,7 @@ enum TravelAlbumOTGPhotoFormatOption: String, CaseIterable, Equatable, Sendable
|
||||
struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
let id: String
|
||||
let sourceId: String
|
||||
var clientPhotoId: String
|
||||
let fileName: String
|
||||
var localPath: String
|
||||
var thumbnailPath: String
|
||||
@ -42,6 +62,7 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
init(
|
||||
id: String,
|
||||
sourceId: String? = nil,
|
||||
clientPhotoId: String = TravelAlbumClientPhotoID.make(),
|
||||
fileName: String,
|
||||
localPath: String,
|
||||
thumbnailPath: String = "",
|
||||
@ -57,6 +78,7 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
) {
|
||||
self.id = id
|
||||
self.sourceId = sourceId ?? id
|
||||
self.clientPhotoId = TravelAlbumClientPhotoID.normalize(clientPhotoId) ?? TravelAlbumClientPhotoID.make()
|
||||
self.fileName = fileName
|
||||
self.localPath = localPath
|
||||
self.thumbnailPath = thumbnailPath
|
||||
@ -71,6 +93,31 @@ struct TravelAlbumOTGPhotoRecord: Codable, Hashable, Sendable {
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id, sourceId, clientPhotoId, fileName, localPath, thumbnailPath, capturedAt
|
||||
case fileSizeBytes, status, progress, errorMessage, albumId, userId, remoteUrl, updatedAt
|
||||
}
|
||||
|
||||
/// 解码本地索引;旧版本缺少 `clientPhotoId` 时先保留为空,由 Store 一次性迁移并回写。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
sourceId = try container.decodeIfPresent(String.self, forKey: .sourceId) ?? id
|
||||
clientPhotoId = try container.decodeIfPresent(String.self, forKey: .clientPhotoId) ?? ""
|
||||
fileName = try container.decode(String.self, forKey: .fileName)
|
||||
localPath = try container.decode(String.self, forKey: .localPath)
|
||||
thumbnailPath = try container.decodeIfPresent(String.self, forKey: .thumbnailPath) ?? ""
|
||||
capturedAt = try container.decode(String.self, forKey: .capturedAt)
|
||||
fileSizeBytes = try container.decodeIfPresent(Int64.self, forKey: .fileSizeBytes) ?? 0
|
||||
status = try container.decode(TravelAlbumOTGUploadStatus.self, forKey: .status)
|
||||
progress = try container.decodeIfPresent(Int.self, forKey: .progress) ?? 0
|
||||
errorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||
albumId = try container.decode(Int.self, forKey: .albumId)
|
||||
userId = try container.decode(String.self, forKey: .userId)
|
||||
remoteUrl = try container.decodeIfPresent(String.self, forKey: .remoteUrl) ?? ""
|
||||
updatedAt = try container.decodeIfPresent(Int64.self, forKey: .updatedAt) ?? 0
|
||||
}
|
||||
|
||||
/// 把中断中的传输恢复为待上传,避免重进页面卡在上传中。
|
||||
func normalizedAfterInterruptedTransfer() -> TravelAlbumOTGPhotoRecord {
|
||||
guard status == .transferring || status == .uploading else { return self }
|
||||
@ -188,9 +235,17 @@ final class TravelAlbumOTGPhotoStore {
|
||||
let decoded = try? JSONDecoder().decode([TravelAlbumOTGPhotoRecord].self, from: data) else {
|
||||
return []
|
||||
}
|
||||
return decoded
|
||||
.filter { $0.albumId == albumId && $0.userId == context.userId }
|
||||
.map { $0.normalizedAfterInterruptedTransfer() }
|
||||
let scoped = decoded.filter { $0.albumId == albumId && $0.userId == context.userId }
|
||||
let migrated = scoped.map { record -> TravelAlbumOTGPhotoRecord in
|
||||
var copy = record
|
||||
copy.clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId)
|
||||
?? TravelAlbumClientPhotoID.make()
|
||||
return copy.normalizedAfterInterruptedTransfer()
|
||||
}
|
||||
if migrated.map(\.clientPhotoId) != scoped.map(\.clientPhotoId) {
|
||||
save(migrated, albumId: albumId)
|
||||
}
|
||||
return migrated
|
||||
.filter { isDisplayable($0, albumId: albumId) }
|
||||
.sorted { $0.capturedAt > $1.capturedAt }
|
||||
}
|
||||
@ -434,6 +489,40 @@ final class TravelAlbumOTGPhotoStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按服务端客户端照片 ID 快照回写本地持久状态。
|
||||
enum TravelAlbumOTGServerStatusPolicy {
|
||||
/// 只处理请求发起时已存在且期间未变化的非活动记录。
|
||||
static func reconcile(
|
||||
records: [TravelAlbumOTGPhotoRecord],
|
||||
baselineRecordsById: [String: TravelAlbumOTGPhotoRecord],
|
||||
activePhotoIds: Set<String>,
|
||||
serverClientPhotoIds: [String],
|
||||
now: Int64
|
||||
) -> [TravelAlbumOTGPhotoRecord] {
|
||||
let serverIds = Set(serverClientPhotoIds.compactMap(TravelAlbumClientPhotoID.normalize))
|
||||
return records.map { record in
|
||||
guard baselineRecordsById[record.id] == record,
|
||||
!activePhotoIds.contains(record.id) else {
|
||||
return record
|
||||
}
|
||||
var copy = record
|
||||
if let clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId),
|
||||
serverIds.contains(clientPhotoId) {
|
||||
copy.status = .uploaded
|
||||
copy.progress = 100
|
||||
copy.errorMessage = nil
|
||||
copy.updatedAt = now
|
||||
} else if record.status == .uploaded {
|
||||
copy.status = .pending
|
||||
copy.progress = 0
|
||||
copy.errorMessage = nil
|
||||
copy.updatedAt = now
|
||||
}
|
||||
return copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容从 `otg_swift` 同步来的边拍边传写入调用。
|
||||
enum AlbumPhotoStorage {
|
||||
/// 写入原片数据到当前账号与相册隔离目录。
|
||||
|
||||
@ -44,6 +44,9 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
|
||||
FileManager.default.fileExists(atPath: record.localPath) else {
|
||||
throw TravelAlbumOTGUploadError.localFileMissing
|
||||
}
|
||||
guard let clientPhotoId = TravelAlbumClientPhotoID.normalize(record.clientPhotoId) else {
|
||||
throw TravelAlbumOTGUploadError.clientPhotoIdMissing
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: URL(fileURLWithPath: record.localPath))
|
||||
let remoteURL = try await uploader.uploadTravelAlbumMaterial(
|
||||
@ -56,7 +59,8 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
|
||||
TravelAlbumUploadMaterialRequest(
|
||||
userEquityTravelId: record.albumId,
|
||||
fileName: record.fileName,
|
||||
fileUrl: remoteURL
|
||||
fileUrl: remoteURL,
|
||||
clientPhotoId: clientPhotoId
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -66,6 +70,7 @@ final class TravelAlbumOTGUploader: TravelAlbumOTGUploading {
|
||||
enum TravelAlbumOTGUploadError: LocalizedError, Equatable {
|
||||
case invalidAlbum
|
||||
case localFileMissing
|
||||
case clientPhotoIdMissing
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
@ -73,6 +78,8 @@ enum TravelAlbumOTGUploadError: LocalizedError, Equatable {
|
||||
return "请先选择有效的相册"
|
||||
case .localFileMissing:
|
||||
return "本地文件不存在,无法上传"
|
||||
case .clientPhotoIdMissing:
|
||||
return "照片标识缺失,请重新导入后重试"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,7 @@ final class WiredCameraTransferViewModel {
|
||||
private let storage: TravelAlbumOTGPhotoStore
|
||||
private let repository: TravelAlbumOTGPhotoRepository
|
||||
private let uploader: any TravelAlbumOTGUploading
|
||||
private let api: any TravelAlbumServing
|
||||
private let appStore: AppStore
|
||||
private let userDefaults: UserDefaults
|
||||
|
||||
@ -85,6 +86,7 @@ final class WiredCameraTransferViewModel {
|
||||
private var isUploading = false
|
||||
private var queuedAutoUploadPhotoIds: Set<String> = []
|
||||
private var suppressSelectedTimeSlotNotification = false
|
||||
private var serverStatusSyncTask: Task<Void, Never>?
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
@ -95,6 +97,7 @@ final class WiredCameraTransferViewModel {
|
||||
connectionManager: (any WiredCameraConnectionManaging)? = nil,
|
||||
storage: TravelAlbumOTGPhotoStore = TravelAlbumOTGPhotoStore(),
|
||||
uploader: (any TravelAlbumOTGUploading)? = nil,
|
||||
api: (any TravelAlbumServing)? = nil,
|
||||
appStore: AppStore = .shared,
|
||||
userDefaults: UserDefaults = .standard
|
||||
) {
|
||||
@ -106,6 +109,7 @@ final class WiredCameraTransferViewModel {
|
||||
self.storage = storage
|
||||
self.repository = TravelAlbumOTGPhotoRepository(storage: storage)
|
||||
self.uploader = uploader ?? TravelAlbumOTGUploader()
|
||||
self.api = api ?? NetworkServices.shared.travelAlbumAPI
|
||||
self.appStore = appStore
|
||||
self.userDefaults = userDefaults
|
||||
self.transferMode = Self.persistedTransferMode(in: userDefaults)
|
||||
@ -225,11 +229,14 @@ final class WiredCameraTransferViewModel {
|
||||
connectionManager.delegate = self
|
||||
syncFromConnectionManager()
|
||||
loadPersistedPhotos()
|
||||
syncServerUploadStatuses()
|
||||
connectionManager.start()
|
||||
}
|
||||
|
||||
/// 页面消失时解绑 UI delegate,并暂停边拍边传轮询,保留底层设备缓存。
|
||||
func stop() {
|
||||
serverStatusSyncTask?.cancel()
|
||||
serverStatusSyncTask = nil
|
||||
connectionManager.unbindDelegate()
|
||||
connectionManager.suspendLiveTransfer()
|
||||
}
|
||||
@ -483,6 +490,39 @@ final class WiredCameraTransferViewModel {
|
||||
applyMergedPhotos()
|
||||
}
|
||||
|
||||
private func syncServerUploadStatuses() {
|
||||
serverStatusSyncTask?.cancel()
|
||||
guard albumId > 0 else { return }
|
||||
let baseline = persistedRecordsById
|
||||
serverStatusSyncTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
let response = try await api.materialClientPhotoIds(userEquityTravelId: albumId)
|
||||
try Task.checkCancellation()
|
||||
let activeIds = Set(persistedRecordsById.values.compactMap { record in
|
||||
record.status == .transferring || record.status == .uploading ? record.id : nil
|
||||
})
|
||||
let reconciled = TravelAlbumOTGServerStatusPolicy.reconcile(
|
||||
records: Array(persistedRecordsById.values),
|
||||
baselineRecordsById: baseline,
|
||||
activePhotoIds: activeIds,
|
||||
serverClientPhotoIds: response.clientPhotoIds,
|
||||
now: Int64(Date().timeIntervalSince1970 * 1000)
|
||||
)
|
||||
let reconciledById = Dictionary(uniqueKeysWithValues: reconciled.map { ($0.id, $0) })
|
||||
guard reconciledById != persistedRecordsById else { return }
|
||||
persistedRecordsById = reconciledById
|
||||
storage.save(reconciled, albumId: albumId)
|
||||
applyMergedPhotos()
|
||||
selectedPhotoIds = selectedPhotoIds.intersection(selectableVisiblePhotoIds)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
OTGLog.error(.connection, "sync server upload statuses failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func applyMergedPhotos() {
|
||||
let persistedItems = persistedRecordsById.values
|
||||
.map { $0.toPhotoItem(storage: storage, albumId: albumId) }
|
||||
|
||||
@ -12,19 +12,16 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
|
||||
private let viewModel: TravelAlbumEntryViewModel
|
||||
private let api: any TravelAlbumServing
|
||||
private var mode: TravelAlbumEntryViewModel.CreateMode = .preShoot
|
||||
private var selectedOrder: TravelAlbumAvailableOrder?
|
||||
private let mode: TravelAlbumEntryViewModel.CreateMode = .preShoot
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
private let titleLabel = UILabel()
|
||||
private let preShootCard = TravelAlbumModeOptionView()
|
||||
private let preOrderCard = TravelAlbumModeOptionView()
|
||||
private let fieldsStack = UIStackView()
|
||||
private let freeCountField = UITextField()
|
||||
private let singlePriceField = UITextField()
|
||||
private let packagePriceField = UITextField()
|
||||
private let orderButton = UIButton(type: .system)
|
||||
private let cancelButton = UIButton(type: .system)
|
||||
private let confirmButton = UIButton(type: .system)
|
||||
|
||||
@ -58,11 +55,6 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
desc: "拍完后分享给用户,用户在小程序上选择性购买",
|
||||
selected: true
|
||||
)
|
||||
preOrderCard.apply(
|
||||
title: "买了再拍",
|
||||
desc: "表示用户已经在小程序上下过单了,绑定对应订单上传后用户可以直接选片",
|
||||
selected: false
|
||||
)
|
||||
|
||||
fieldsStack.axis = .vertical
|
||||
fieldsStack.spacing = 14
|
||||
@ -71,23 +63,12 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
configureTextField(singlePriceField, placeholder: "请输入单张照片价格", keyboardType: .decimalPad)
|
||||
configureTextField(packagePriceField, placeholder: "请输入打包价格", keyboardType: .decimalPad)
|
||||
|
||||
orderButton.contentHorizontalAlignment = .left
|
||||
orderButton.setTitle("请选择订单", for: .normal)
|
||||
orderButton.setTitleColor(AppColor.textTertiary, for: .normal)
|
||||
orderButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
orderButton.layer.cornerRadius = 8
|
||||
orderButton.layer.borderColor = AppColor.border.cgColor
|
||||
orderButton.layer.borderWidth = 1
|
||||
orderButton.setConfigurationContentInsets(
|
||||
NSDirectionalEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
|
||||
)
|
||||
|
||||
configureActionButton(cancelButton, title: "取消", backgroundColor: UIColor(hex: 0xF4F4F4), titleColor: AppColor.textSecondary)
|
||||
configureActionButton(confirmButton, title: "确定", backgroundColor: AppColor.primary, titleColor: .white)
|
||||
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
[titleLabel, preShootCard, preOrderCard, fieldsStack, cancelButton, confirmButton].forEach(contentView.addSubview)
|
||||
[titleLabel, preShootCard, fieldsStack, cancelButton, confirmButton].forEach(contentView.addSubview)
|
||||
rebuildFields()
|
||||
}
|
||||
|
||||
@ -107,12 +88,8 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
make.top.equalTo(titleLabel.snp.bottom).offset(18)
|
||||
make.leading.trailing.equalToSuperview().inset(16)
|
||||
}
|
||||
preOrderCard.snp.makeConstraints { make in
|
||||
make.top.equalTo(preShootCard.snp.bottom).offset(10)
|
||||
make.leading.trailing.equalTo(preShootCard)
|
||||
}
|
||||
fieldsStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(preOrderCard.snp.bottom).offset(18)
|
||||
make.top.equalTo(preShootCard.snp.bottom).offset(18)
|
||||
make.leading.trailing.equalTo(preShootCard)
|
||||
}
|
||||
cancelButton.snp.makeConstraints { make in
|
||||
@ -130,9 +107,6 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
preShootCard.addTarget(self, action: #selector(preShootTapped), for: .touchUpInside)
|
||||
preOrderCard.addTarget(self, action: #selector(preOrderTapped), for: .touchUpInside)
|
||||
orderButton.addTarget(self, action: #selector(orderTapped), for: .touchUpInside)
|
||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
||||
[freeCountField, singlePriceField, packagePriceField].forEach {
|
||||
@ -171,13 +145,9 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
|
||||
private func rebuildFields() {
|
||||
fieldsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
if mode == .preShoot {
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "免费张数", required: false, field: freeCountField))
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "单张照片价格(元)", required: true, field: singlePriceField))
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "打包价格(元)", required: false, field: packagePriceField))
|
||||
} else {
|
||||
fieldsStack.addArrangedSubview(makeOrderGroup())
|
||||
}
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "免费张数", required: false, field: freeCountField))
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "单张照片价格(元)", required: true, field: singlePriceField))
|
||||
fieldsStack.addArrangedSubview(makeFieldGroup(title: "打包价格(元)", required: false, field: packagePriceField))
|
||||
}
|
||||
|
||||
private func makeFieldGroup(title: String, required: Bool, field: UITextField) -> UIView {
|
||||
@ -196,23 +166,6 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
return container
|
||||
}
|
||||
|
||||
private func makeOrderGroup() -> UIView {
|
||||
let container = UIView()
|
||||
let label = UILabel()
|
||||
label.attributedText = fieldTitle("绑定订单", required: false)
|
||||
container.addSubview(label)
|
||||
container.addSubview(orderButton)
|
||||
label.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalToSuperview()
|
||||
}
|
||||
orderButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(label.snp.bottom).offset(8)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
make.height.greaterThanOrEqualTo(46)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
private func fieldTitle(_ text: String, required: Bool) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString(
|
||||
string: text,
|
||||
@ -224,58 +177,6 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
return result
|
||||
}
|
||||
|
||||
private func updateMode(_ newMode: TravelAlbumEntryViewModel.CreateMode) {
|
||||
mode = newMode
|
||||
preShootCard.apply(
|
||||
title: "先拍再买",
|
||||
desc: "拍完后分享给用户,用户在小程序上选择性购买",
|
||||
selected: mode == .preShoot
|
||||
)
|
||||
preOrderCard.apply(
|
||||
title: "买了再拍",
|
||||
desc: "表示用户已经在小程序上下过单了,绑定对应订单上传后用户可以直接选片",
|
||||
selected: mode == .preOrder
|
||||
)
|
||||
rebuildFields()
|
||||
}
|
||||
|
||||
private func updateOrderButton() {
|
||||
guard let order = selectedOrder else {
|
||||
orderButton.setTitle("请选择订单", for: .normal)
|
||||
orderButton.setTitleColor(AppColor.textTertiary, for: .normal)
|
||||
return
|
||||
}
|
||||
let title = "\(order.projectName.isEmpty ? "未命名项目" : order.projectName)\n手机号:\(order.userPhone)\n订单号:\(order.orderNumber)"
|
||||
orderButton.setTitle(title, for: .normal)
|
||||
orderButton.setTitleColor(AppColor.textPrimary, for: .normal)
|
||||
orderButton.titleLabel?.numberOfLines = 3
|
||||
}
|
||||
|
||||
@objc private func preShootTapped() {
|
||||
updateMode(.preShoot)
|
||||
}
|
||||
|
||||
@objc private func preOrderTapped() {
|
||||
updateMode(.preOrder)
|
||||
}
|
||||
|
||||
@objc private func orderTapped() {
|
||||
let alert = UIAlertController(title: "绑定订单", message: nil, preferredStyle: .actionSheet)
|
||||
if viewModel.availableOrders.isEmpty {
|
||||
alert.addAction(UIAlertAction(title: "暂无可绑定订单", style: .default))
|
||||
} else {
|
||||
viewModel.availableOrders.forEach { order in
|
||||
let title = "\(order.projectName.isEmpty ? "未命名项目" : order.projectName) \(order.userPhone)"
|
||||
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||||
self?.selectedOrder = order
|
||||
self?.updateOrderButton()
|
||||
})
|
||||
}
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
@objc private func cancelTapped() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
@ -287,7 +188,7 @@ final class CreateTravelAlbumSheetViewController: BaseViewController {
|
||||
freeCount: freeCountField.text ?? "",
|
||||
singlePrice: singlePriceField.text ?? "",
|
||||
packagePrice: packagePriceField.text ?? "",
|
||||
order: selectedOrder,
|
||||
order: nil,
|
||||
api: api
|
||||
)
|
||||
await MainActor.run {
|
||||
|
||||
@ -421,13 +421,19 @@ final class WildReportRiskMapViewController: BaseViewController {
|
||||
let avatarWrap = UIView()
|
||||
avatarWrap.backgroundColor = AppColor.success.withAlphaComponent(0.12)
|
||||
avatarWrap.layer.cornerRadius = 28
|
||||
let avatar = UIImageView(image: UIImage(systemName: clue.avatar ?? "person.crop.circle.fill"))
|
||||
let placeholder = UIImage(systemName: "person.crop.circle.fill")
|
||||
let avatar = UIImageView(image: placeholder)
|
||||
avatar.tintColor = AppColor.success
|
||||
avatar.contentMode = .scaleAspectFit
|
||||
avatar.contentMode = .scaleAspectFill
|
||||
avatar.clipsToBounds = true
|
||||
avatar.layer.cornerRadius = 28
|
||||
let avatarURLString = clue.avatar?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
if let avatarURL = URL(string: avatarURLString), !avatarURLString.isEmpty {
|
||||
avatar.kf.setImage(with: avatarURL, placeholder: placeholder)
|
||||
}
|
||||
avatarWrap.addSubview(avatar)
|
||||
avatar.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
make.size.equalTo(42)
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
avatarWrap.snp.makeConstraints { make in
|
||||
make.size.equalTo(56)
|
||||
|
||||
Reference in New Issue
Block a user