// // SampleManagementModels.swift // suixinkan // import Foundation /// 样片列表筛选状态,对齐 Android `SampleListViewModel` 的四档筛选。 enum SampleFilterStatus: Int, CaseIterable, Sendable, Equatable { case all = 0 case approved = 1 case pending = 2 case rejected = 3 /// 展示文案。 var title: String { switch self { case .all: "全部" case .approved: "已通过" case .pending: "待审核" case .rejected: "未通过" } } /// 服务端 `status` 参数,Android 固定映射为:全部 4、已审核 1、待审核 0、未通过 2。 var apiStatus: Int { switch self { case .all: 4 case .approved: 1 case .pending: 0 case .rejected: 2 } } } /// 样片素材类型,对齐 Android `mediaType`:1 图片,2 视频。 enum SampleMediaType: Int, CaseIterable, Sendable, Equatable { case image = 1 case video = 2 /// 展示文案。 var title: String { switch self { case .image: "图片" case .video: "视频" } } /// 最大可选数量。 var maxCount: Int { switch self { case .image: 9 case .video: 1 } } /// OSS 文件类型,Android `UploadTaskEntity.fileType` 中视频为 1、图片为 2。 var uploadFileType: Int { switch self { case .image: 2 case .video: 1 } } } /// 样片列表项,对齐 Android `MaterialItemEntity`。 struct SampleMaterialItem: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int let name: String let coverUrl: String let createdAt: String let status: Int let downloadCount: Int let likesCount: Int let collectCount: Int let shareCount: Int let scenicSpotName: String let projectName: String let listingStatus: Int enum CodingKeys: String, CodingKey { case id case name case coverUrl = "cover_url" case createdAt = "created_at" case status case downloadCount = "download_count" case likesCount = "likes_count" case collectCount = "collect_count" case shareCount = "share_count" case scenicSpotName = "scenic_spot_name" case projectName = "project_name" case listingStatus = "listing_status" } init( id: Int = 0, name: String = "", coverUrl: String = "", createdAt: String = "", status: Int = 0, downloadCount: Int = 0, likesCount: Int = 0, collectCount: Int = 0, shareCount: Int = 0, scenicSpotName: String = "", projectName: String = "", listingStatus: Int = 0 ) { self.id = id self.name = name self.coverUrl = coverUrl self.createdAt = createdAt self.status = status self.downloadCount = downloadCount self.likesCount = likesCount self.collectCount = collectCount self.shareCount = shareCount self.scenicSpotName = scenicSpotName self.projectName = projectName self.listingStatus = listingStatus } /// 是否已上架。 var isListed: Bool { listingStatus == 1 } } /// 样片列表响应,对齐 Android `MaterialListResponse`。 struct SampleMaterialListResponse: Decodable, Sendable, Equatable { let total: Int let list: [SampleMaterialItem] let order: SampleMaterialOrderInfo? init(total: Int = 0, list: [SampleMaterialItem] = [], order: SampleMaterialOrderInfo? = nil) { self.total = total self.list = list self.order = order } } /// 样片列表统计信息,对齐 Android `MaterialOrderInfo`。 struct SampleMaterialOrderInfo: Decodable, Sendable, Equatable { let totalNum: Int let avgOrderAmount: String let refundTotal: String let avgChange: Int enum CodingKeys: String, CodingKey { case totalNum = "total_num" case avgOrderAmount = "avg_order_amount" case refundTotal = "refund_total" case avgChange = "avg_change" } init(totalNum: Int = 0, avgOrderAmount: String = "0", refundTotal: String = "0", avgChange: Int = 0) { self.totalNum = totalNum self.avgOrderAmount = avgOrderAmount self.refundTotal = refundTotal self.avgChange = avgChange } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) totalNum = try container.decodeIfPresent(Int.self, forKey: .totalNum) ?? 0 avgOrderAmount = try container.decodeIfPresent(String.self, forKey: .avgOrderAmount) ?? "0" refundTotal = try container.decodeIfPresent(String.self, forKey: .refundTotal) ?? "0" avgChange = try container.decodeIfPresent(Int.self, forKey: .avgChange) ?? 0 } } /// 样片详情响应,对齐 Android `SampleDetailResponse`。 struct SampleDetail: Decodable, Sendable, Equatable { let id: Int let name: String? let type: Int? let coverUrl: String? let coverSize: Int? let description: String? let scenicSpotId: Int? let scenicAreaId: Int? let uploaderId: Int? let status: Int let likesCount: Int let downloadCount: Int let collectCount: Int let createdAt: String? let updatedAt: String? let deletedAt: String? let listingStatus: Int let reviewNotes: String? let reviewTime: String? let reviewerId: Int? let provinceId: Int? let cityId: Int? let shareCount: Int let uploaderName: String? let uploaderAvatar: String? let uploaderDescription: String? let platformCertification: Bool let scenicCertification: Bool let mediaList: [SampleDetailMediaItem]? let scenicName: String? let scenicCoverImg: String? let scenicPlanesNum: Int let scenicPhotographerNum: Int let projectCover: String? let projectName: String? let projectPrice: Double? let projectPriceDeposit: String? let reviewer: String? enum CodingKeys: String, CodingKey { case id case name case type case coverUrl = "cover_url" case coverSize = "cover_size" case description case scenicSpotId = "scenic_spot_id" case scenicAreaId = "scenic_area_id" case uploaderId = "uploader_id" case status case likesCount = "likes_count" case downloadCount = "download_count" case collectCount = "collect_count" case createdAt = "created_at" case updatedAt = "updated_at" case deletedAt = "deleted_at" case listingStatus = "listing_status" case reviewNotes = "review_notes" case reviewTime = "review_time" case reviewerId = "reviewer_id" case provinceId = "province_id" case cityId = "city_id" case shareCount = "share_count" case uploaderName = "uploader_name" case uploaderAvatar = "uploader_avatar" case uploaderDescription = "uploader_description" case platformCertification = "platform_certification" case scenicCertification = "scenic_certification" case mediaList = "media_list" case scenicName = "scenic_name" case scenicCoverImg = "scenic_cover_img" case scenicPlanesNum = "scenic_planes_num" case scenicPhotographerNum = "scenic_photographer_num" case projectCover = "project_cover" case projectName = "project_name" case projectPrice = "project_price" case projectPriceDeposit = "project_price_deposit" case reviewer } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 name = try container.decodeIfPresent(String.self, forKey: .name) type = try container.decodeIfPresent(Int.self, forKey: .type) coverUrl = try container.decodeIfPresent(String.self, forKey: .coverUrl) coverSize = try container.decodeIfPresent(Int.self, forKey: .coverSize) description = try container.decodeIfPresent(String.self, forKey: .description) scenicSpotId = try container.decodeIfPresent(Int.self, forKey: .scenicSpotId) scenicAreaId = try container.decodeIfPresent(Int.self, forKey: .scenicAreaId) uploaderId = try container.decodeIfPresent(Int.self, forKey: .uploaderId) status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0 likesCount = try container.decodeIfPresent(Int.self, forKey: .likesCount) ?? 0 downloadCount = try container.decodeIfPresent(Int.self, forKey: .downloadCount) ?? 0 collectCount = try container.decodeIfPresent(Int.self, forKey: .collectCount) ?? 0 createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) updatedAt = try container.decodeIfPresent(String.self, forKey: .updatedAt) deletedAt = try container.decodeIfPresent(String.self, forKey: .deletedAt) listingStatus = try container.decodeIfPresent(Int.self, forKey: .listingStatus) ?? 0 reviewNotes = try container.decodeIfPresent(String.self, forKey: .reviewNotes) reviewTime = try container.decodeIfPresent(String.self, forKey: .reviewTime) reviewerId = try container.decodeIfPresent(Int.self, forKey: .reviewerId) provinceId = try container.decodeIfPresent(Int.self, forKey: .provinceId) cityId = try container.decodeIfPresent(Int.self, forKey: .cityId) shareCount = try container.decodeIfPresent(Int.self, forKey: .shareCount) ?? 0 uploaderName = try container.decodeIfPresent(String.self, forKey: .uploaderName) uploaderAvatar = try container.decodeIfPresent(String.self, forKey: .uploaderAvatar) uploaderDescription = try container.decodeIfPresent(String.self, forKey: .uploaderDescription) platformCertification = try container.decodeIfPresent(Bool.self, forKey: .platformCertification) ?? false scenicCertification = try container.decodeIfPresent(Bool.self, forKey: .scenicCertification) ?? false mediaList = try container.decodeIfPresent([SampleDetailMediaItem].self, forKey: .mediaList) scenicName = try container.decodeIfPresent(String.self, forKey: .scenicName) scenicCoverImg = try container.decodeIfPresent(String.self, forKey: .scenicCoverImg) scenicPlanesNum = try container.decodeIfPresent(Int.self, forKey: .scenicPlanesNum) ?? 0 scenicPhotographerNum = try container.decodeIfPresent(Int.self, forKey: .scenicPhotographerNum) ?? 0 projectCover = try container.decodeIfPresent(String.self, forKey: .projectCover) projectName = try container.decodeIfPresent(String.self, forKey: .projectName) projectPrice = try container.decodeFlexibleDouble(forKey: .projectPrice) projectPriceDeposit = try container.decodeFlexibleString(forKey: .projectPriceDeposit) reviewer = try container.decodeIfPresent(String.self, forKey: .reviewer) } } /// 样片详情媒体项,对齐 Android `MaterialDetailMediaItem`。 struct SampleDetailMediaItem: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int let originalName: String? let paidMaterialId: Int? let ossUrl: String? let thumbnailUrl: String? let type: Int let size: Int64? let createdAt: String? let updatedAt: String? let deletedAt: String? let likesCount: Int let downloadCount: Int let showUrl: String? let width: Int? let height: Int? enum CodingKeys: String, CodingKey { case id case originalName = "original_name" case paidMaterialId = "paid_material_id" case ossUrl = "oss_url" case thumbnailUrl = "thumbnail_url" case type case size case createdAt = "created_at" case updatedAt = "updated_at" case deletedAt = "deleted_at" case likesCount = "likes_count" case downloadCount = "download_count" case showUrl = "show_url" case width case height } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0 originalName = try container.decodeIfPresent(String.self, forKey: .originalName) paidMaterialId = try container.decodeIfPresent(Int.self, forKey: .paidMaterialId) ossUrl = try container.decodeIfPresent(String.self, forKey: .ossUrl) thumbnailUrl = try container.decodeIfPresent(String.self, forKey: .thumbnailUrl) type = try container.decodeIfPresent(Int.self, forKey: .type) ?? 0 if let numericSize = try? container.decode(Int64.self, forKey: .size) { size = numericSize } else if let stringSize = try? container.decode(String.self, forKey: .size) { size = Int64(stringSize) } else { size = nil } createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) updatedAt = try container.decodeIfPresent(String.self, forKey: .updatedAt) deletedAt = try container.decodeIfPresent(String.self, forKey: .deletedAt) likesCount = try container.decodeIfPresent(Int.self, forKey: .likesCount) ?? 0 downloadCount = try container.decodeIfPresent(Int.self, forKey: .downloadCount) ?? 0 showUrl = try container.decodeIfPresent(String.self, forKey: .showUrl) width = try container.decodeIfPresent(Int.self, forKey: .width) height = try container.decodeIfPresent(Int.self, forKey: .height) } /// 详情轮播优先展示地址。 var displayURL: String { (ossUrl?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty) ?? (showUrl?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty) ?? "" } /// 返回可展示的媒体类型;服务端媒体项为 0 时按 Android 规则回退到详情顶层类型。 func resolvedType(fallback: Int) -> Int { type == 0 ? fallback : type } } /// 样片关联项目项,对齐 Android `ProjectItem`。 struct SampleProjectItem: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int let type: Int let typeName: String let status: Int let statusName: String let name: String let coverProject: String let coverVideo: String let price: String let otPrice: String let priceDeposit: String let label: String let attrLabel: [String]? let creatorId: Int enum CodingKeys: String, CodingKey { case id case type case typeName = "type_name" case status case statusName = "status_name" case name case coverProject = "cover_project" case coverVideo = "cover_video" case price case otPrice = "ot_price" case priceDeposit = "price_deposit" case label case attrLabel = "attr_label" case creatorId = "creator_id" } init( id: Int = 0, type: Int = 0, typeName: String = "", status: Int = 0, statusName: String = "", name: String = "", coverProject: String = "", coverVideo: String = "", price: String = "", otPrice: String = "", priceDeposit: String = "", label: String = "", attrLabel: [String]? = nil, creatorId: Int = 0 ) { self.id = id self.type = type self.typeName = typeName self.status = status self.statusName = statusName self.name = name self.coverProject = coverProject self.coverVideo = coverVideo self.price = price self.otPrice = otPrice self.priceDeposit = priceDeposit self.label = label self.attrLabel = attrLabel self.creatorId = creatorId } } /// 样片关联项目分页响应,对齐 Android `ProjectListData`。 struct SampleProjectListResponse: Decodable, Sendable, Equatable { let list: [SampleProjectItem] let total: Int init(list: [SampleProjectItem] = [], total: Int = 0) { self.list = list self.total = total } } /// 样片上传请求,对齐 Android `UploadMaterialRequest`。 struct SampleUploadMaterialRequest: Encodable, Sendable, Equatable { let name: String let type: String let mediaType: String let mediaList: [SampleUploadMediaItem] let coverUrl: String let coverSize: String let scenicSpotId: Int64 let description: String let materialTag: String let projectId: Int? enum CodingKeys: String, CodingKey { case name case type case mediaType = "media_type" case mediaList = "media_list" case coverUrl = "cover_url" case coverSize = "cover_size" case scenicSpotId = "scenic_spot_id" case description case materialTag = "material_tag" case projectId = "project_id" } init( name: String, mediaType: SampleMediaType, mediaList: [SampleUploadMediaItem], coverUrl: String, coverSize: String, scenicSpotId: Int64, projectId: Int? ) { self.name = name self.type = "2" self.mediaType = String(mediaType.rawValue) self.mediaList = mediaList self.coverUrl = coverUrl self.coverSize = coverSize self.scenicSpotId = scenicSpotId self.description = "" self.materialTag = "" self.projectId = projectId } } /// 样片上传素材项,对齐 Android `MediaItem`。 struct SampleUploadMediaItem: Encodable, Sendable, Equatable { let originalName: String let ossUrl: String let size: Int64 let fileWidthSize: SampleUploadFileSize enum CodingKeys: String, CodingKey { case originalName = "original_name" case ossUrl = "oss_url" case size case fileWidthSize = "file_width_size" } } /// 样片上传文件宽高,对齐 Android `FileSize`。 struct SampleUploadFileSize: Encodable, Sendable, Equatable { let width: Int let height: Int } /// 上传页本地媒体项,不依赖 UIKit,供 ViewModel 管理上传状态。 struct SampleUploadedMediaItem: Sendable, Equatable, Hashable, Identifiable { let id: String let data: Data let thumbnailData: Data? let fileName: String let size: Int64 let width: Int let height: Int var ossUrl: String var isUploading: Bool var uploadProgress: Int init( id: String = UUID().uuidString, data: Data, thumbnailData: Data? = nil, fileName: String, size: Int64? = nil, width: Int = 0, height: Int = 0, ossUrl: String = "", isUploading: Bool = false, uploadProgress: Int = 0 ) { self.id = id self.data = data self.thumbnailData = thumbnailData self.fileName = fileName self.size = size ?? Int64(data.count) self.width = width self.height = height self.ossUrl = ossUrl self.isUploading = isUploading self.uploadProgress = uploadProgress } } /// 样片上传进度弹窗状态。 struct SampleUploadDialogState: Sendable, Equatable { let title: String let progress: Int } /// 样片 UI 展示格式化工具。 enum SampleDisplayFormatter { /// 格式化计数,超过 1000 按 Android 规则显示 k。 static func formatCount(_ count: Int) -> String { guard count >= 1000 else { return String(count) } let value = Double(count) / 1000.0 if value.truncatingRemainder(dividingBy: 1) == 0 { return "\(Int(value))k" } return String(format: "%.1fk", value) } /// 审核状态文案。 static func reviewStatusText(_ status: Int) -> String { switch status { case 1: "已通过" case 0: "待审核" case 2: "未通过" default: "未知" } } } private extension KeyedDecodingContainer { func decodeFlexibleDouble(forKey key: Key) throws -> Double? { if let value = try? decode(Double.self, forKey: key) { return value } if let string = try? decode(String.self, forKey: key) { return Double(string) } return nil } func decodeFlexibleString(forKey key: Key) throws -> String? { if let value = try? decode(String.self, forKey: key) { return value } if let value = try? decode(Double.self, forKey: key) { return String(value) } if let value = try? decode(Int.self, forKey: key) { return String(value) } return nil } } private extension String { var nonEmpty: String? { isEmpty ? nil : self } }