Files
suixinkan_uikit/suixinkan/Features/SampleManagement/Models/SampleManagementModels.swift
汉秋 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 }
}