Files
suixinkan_uikit/suixinkan/Features/SampleManagement/Models/SampleManagementModels.swift
T
lujiuyinandCursor 07b2b9d459 修复样片详情解析并优化列表上下架交互。
兼容 Android 媒体项 size 字段格式,增强灵活解码容错,并统一样片列表开关确认逻辑与安全区布局。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:14:40 +08:00

612 lines
20 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 }
}