Files
suixinkan_ios_new/suixinkan/Features/Assets/Models/AssetsModels.swift
汉秋 a48eb76ba4 Add Assets module with cloud storage and media library flows.
Wire home routing for cloud and material management pages, move shared cloud models into Assets, and add API/view model tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 16:10:20 +08:00

757 lines
24 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.

//
// AssetsModels.swift
// suixinkan
//
// Created by Codex on 2026/6/23.
//
import Foundation
///
struct CloudDriveFile: Decodable, Identifiable, Hashable {
let id: Int
let parentFolderId: Int
let fileUrl: String
let coverUrl: String
let updatedAt: String
let name: String
let createdAt: String
let childNum: Int
let type: Int
let fileSize: Int64
///
var isFolder: Bool { type == 99 }
///
var isVideo: Bool {
type == 1 || Self.hasVideoExtension(name) || Self.hasVideoExtension(fileUrl)
}
///
var isImage: Bool {
type == 2 || Self.hasImageExtension(name) || Self.hasImageExtension(fileUrl)
}
///
var previewURLString: String {
coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
}
///
private static func hasVideoExtension(_ value: String) -> Bool {
["mp4", "mov", "m4v", "avi"].contains(pathExtension(for: value))
}
///
private static func hasImageExtension(_ value: String) -> Bool {
["png", "jpg", "jpeg", "heic", "heif", "webp"].contains(pathExtension(for: value))
}
/// URL
private static func pathExtension(for value: String) -> String {
URL(string: value)?.pathExtension.lowercased() ?? URL(fileURLWithPath: value).pathExtension.lowercased()
}
/// 线
enum CodingKeys: String, CodingKey {
case id
case parentFolderId = "parent_folder_id"
case fileUrl = "file_url"
case coverUrl = "cover_url"
case updatedAt = "updated_at"
case name
case createdAt = "created_at"
case childNum = "child_num"
case type
case fileSize = "file_size"
}
///
init(id: Int, name: String) {
self.id = id
parentFolderId = 0
fileUrl = ""
coverUrl = ""
updatedAt = ""
self.name = name
createdAt = ""
childNum = 0
type = 99
fileSize = 0
}
///
init(
id: Int,
parentFolderId: Int,
fileUrl: String,
coverUrl: String = "",
updatedAt: String = "",
name: String,
createdAt: String = "",
childNum: Int = 0,
type: Int,
fileSize: Int64 = 0
) {
self.id = id
self.parentFolderId = parentFolderId
self.fileUrl = fileUrl
self.coverUrl = coverUrl
self.updatedAt = updatedAt
self.name = name
self.createdAt = createdAt
self.childNum = childNum
self.type = type
self.fileSize = fileSize
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
parentFolderId = try container.decodeLossyInt(forKey: .parentFolderId) ?? 0
fileUrl = try container.decodeLossyString(forKey: .fileUrl)
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
updatedAt = try container.decodeLossyString(forKey: .updatedAt)
name = try container.decodeLossyString(forKey: .name)
createdAt = try container.decodeLossyString(forKey: .createdAt)
childNum = try container.decodeLossyInt(forKey: .childNum) ?? 0
type = try container.decodeLossyInt(forKey: .type) ?? 0
fileSize = try container.decodeLossyInt64(forKey: .fileSize) ?? 0
}
}
///
enum CloudDriveFilter: Int, CaseIterable, Identifiable {
case all = 0
case video = 1
case image = 2
case folder = 99
var id: Int { rawValue }
///
var title: String {
switch self {
case .all:
"全部"
case .video:
"视频"
case .image:
"图片"
case .folder:
"文件夹"
}
}
}
///
enum CloudDriveSort: Int, CaseIterable, Identifiable {
case updatedDesc = 2
case createdDesc = 1
var id: Int { rawValue }
///
var title: String {
switch self {
case .updatedDesc:
"最近更新"
case .createdDesc:
"创建时间"
}
}
}
///
struct CloudFolderCreateRequest: Encodable, Equatable {
let parentFolderId: Int
let name: String
/// 线
enum CodingKeys: String, CodingKey {
case parentFolderId = "parent_folder_id"
case name
}
}
///
struct CloudFileUploadRequest: Encodable, Equatable {
let parentFolderId: Int
let fileUrl: String
let fileName: String
/// 线
enum CodingKeys: String, CodingKey {
case parentFolderId = "parent_folder_id"
case fileUrl = "file_url"
case fileName = "file_name"
}
}
/// /
struct CloudFileActionItem: Encodable, Equatable {
let id: Int
let type: Int
}
///
struct CloudFileDeleteRequest: Encodable, Equatable {
let list: [CloudFileActionItem]
}
///
struct CloudFileMoveRequest: Encodable, Equatable {
let targetFolderId: Int
let list: [CloudFileActionItem]
/// 线
enum CodingKeys: String, CodingKey {
case targetFolderId = "target_folder_id"
case list
}
}
///
struct CloudFolderModifyRequest: Encodable, Equatable {
let id: Int
let name: String
}
///
struct CloudFileModifyRequest: Encodable, Equatable {
let id: Int
let fileName: String
/// 线
enum CodingKeys: String, CodingKey {
case id
case fileName = "file_name"
}
}
///
struct CheckCloudUploadResponse: Decodable, Equatable {
let canUpload: Bool
let reason: String
/// 线
enum CodingKeys: String, CodingKey {
case canUpload = "can_upload"
case reason
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
canUpload = try container.decodeLossyBool(forKey: .canUpload) ?? true
reason = try container.decodeLossyString(forKey: .reason)
}
}
/// PhotosPicker
struct CloudLocalUploadFile: Identifiable, Equatable {
let id = UUID()
let data: Data
let fileName: String
let fileType: Int
}
///
enum CloudTransferDirection: String, Codable, Equatable {
case upload
case download
}
///
enum CloudTransferStatus: String, Codable, Equatable {
case running
case success
case failed
}
/// App
struct CloudTransferItem: Identifiable, Equatable {
let id: UUID
let fileName: String
let direction: CloudTransferDirection
var progress: Int
var status: CloudTransferStatus
var message: String
let createdAt: Date
///
init(
id: UUID = UUID(),
fileName: String,
direction: CloudTransferDirection,
progress: Int = 0,
status: CloudTransferStatus = .running,
message: String = "",
createdAt: Date = Date()
) {
self.id = id
self.fileName = fileName
self.direction = direction
self.progress = progress
self.status = status
self.message = message
self.createdAt = createdAt
}
}
///
enum MediaLibraryKind: Int, CaseIterable, Identifiable {
case material = 1
var id: Int { rawValue }
///
var title: String {
switch self {
case .material:
"素材管理"
}
}
}
///
enum MediaLibraryAuditFilter: Int, CaseIterable, Identifiable {
case all = -1
case pending = 0
case approved = 1
case rejected = 2
var id: Int { rawValue }
///
var title: String {
switch self {
case .all:
"全部"
case .pending:
"待审核"
case .approved:
"已通过"
case .rejected:
"未通过"
}
}
}
///
enum MediaListingStatus: Int, Codable, Equatable {
case offline = 0
case online = 1
///
var toggled: MediaListingStatus {
self == .online ? .offline : .online
}
///
var title: String {
self == .online ? "已上架" : "未上架"
}
}
///
struct MediaLibraryListResponse: Decodable, Equatable {
let total: Int
let list: [MediaLibraryItem]
let order: MediaLibraryOrderInfo?
///
enum CodingKeys: String, CodingKey {
case total
case list
case order
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decodeLossyInt(forKey: .total) ?? 0
list = (try? container.decodeIfPresent([MediaLibraryItem].self, forKey: .list)) ?? []
order = try? container.decodeIfPresent(MediaLibraryOrderInfo.self, forKey: .order)
}
}
///
struct MediaLibraryOrderInfo: Decodable, Equatable {
let totalNum: Int
let avgOrderAmount: Double
let refundTotal: Double
let avgChange: Double
/// 线
enum CodingKeys: String, CodingKey {
case totalNum = "total_num"
case avgOrderAmount = "avg_order_amount"
case refundTotal = "refund_total"
case avgChange = "avg_change"
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
totalNum = try container.decodeLossyInt(forKey: .totalNum) ?? 0
avgOrderAmount = try container.decodeLossyDouble(forKey: .avgOrderAmount) ?? 0
refundTotal = try container.decodeLossyDouble(forKey: .refundTotal) ?? 0
avgChange = try container.decodeLossyDouble(forKey: .avgChange) ?? 0
}
}
///
struct MediaLibraryItem: Decodable, Identifiable, Hashable {
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
///
var isApproved: Bool { status == 1 }
///
var listing: MediaListingStatus {
MediaListingStatus(rawValue: listingStatus) ?? .offline
}
/// 线
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(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
name = try container.decodeLossyString(forKey: .name)
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
createdAt = try container.decodeLossyString(forKey: .createdAt)
status = try container.decodeLossyInt(forKey: .status) ?? 0
downloadCount = try container.decodeLossyInt(forKey: .downloadCount) ?? 0
likesCount = try container.decodeLossyInt(forKey: .likesCount) ?? 0
collectCount = try container.decodeLossyInt(forKey: .collectCount) ?? 0
shareCount = try container.decodeLossyInt(forKey: .shareCount) ?? 0
scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName)
projectName = try container.decodeLossyString(forKey: .projectName)
listingStatus = try container.decodeLossyInt(forKey: .listingStatus) ?? 0
}
}
///
struct MediaLibraryDetail: Decodable, Equatable {
let id: Int
let name: String
let coverUrl: String
let description: String
let status: Int
let listingStatus: Int
let likesCount: Int
let downloadCount: Int
let collectCount: Int
let shareCount: Int
let uploaderName: String
let createdAt: String
let mediaList: [MediaLibraryMediaItem]
let scenicName: String
let projectName: String
///
var isApproved: Bool { status == 1 }
///
var listing: MediaListingStatus {
MediaListingStatus(rawValue: listingStatus) ?? .offline
}
/// 线
enum CodingKeys: String, CodingKey {
case id
case name
case coverUrl = "cover_url"
case description
case status
case listingStatus = "listing_status"
case likesCount = "likes_count"
case downloadCount = "download_count"
case collectCount = "collect_count"
case shareCount = "share_count"
case uploaderName = "uploader_name"
case createdAt = "created_at"
case mediaList = "media_list"
case scenicName = "scenic_name"
case projectName = "project_name"
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
name = try container.decodeLossyString(forKey: .name)
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
description = try container.decodeLossyString(forKey: .description)
status = try container.decodeLossyInt(forKey: .status) ?? 0
listingStatus = try container.decodeLossyInt(forKey: .listingStatus) ?? 0
likesCount = try container.decodeLossyInt(forKey: .likesCount) ?? 0
downloadCount = try container.decodeLossyInt(forKey: .downloadCount) ?? 0
collectCount = try container.decodeLossyInt(forKey: .collectCount) ?? 0
shareCount = try container.decodeLossyInt(forKey: .shareCount) ?? 0
uploaderName = try container.decodeLossyString(forKey: .uploaderName)
createdAt = try container.decodeLossyString(forKey: .createdAt)
mediaList = (try? container.decodeIfPresent([MediaLibraryMediaItem].self, forKey: .mediaList)) ?? []
scenicName = try container.decodeLossyString(forKey: .scenicName)
projectName = try container.decodeLossyString(forKey: .projectName)
}
}
///
struct MediaLibraryMediaItem: Decodable, Identifiable, Hashable {
let id: Int
let originalName: String
let ossUrl: String
let thumbnailUrl: String
let type: Int
let size: Int64
let showUrl: String
///
var isVideo: Bool {
type == 1 || ["mp4", "mov", "m4v", "avi"].contains(URL(string: ossUrl)?.pathExtension.lowercased() ?? "")
}
/// 线
enum CodingKeys: String, CodingKey {
case id
case originalName = "original_name"
case ossUrl = "oss_url"
case thumbnailUrl = "thumbnail_url"
case type
case size
case showUrl = "show_url"
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
originalName = try container.decodeLossyString(forKey: .originalName)
ossUrl = try container.decodeLossyString(forKey: .ossUrl)
thumbnailUrl = try container.decodeLossyString(forKey: .thumbnailUrl)
type = try container.decodeLossyInt(forKey: .type) ?? 0
size = try container.decodeLossyInt64(forKey: .size) ?? 0
showUrl = try container.decodeLossyString(forKey: .showUrl)
}
}
///
struct MediaAlbumFileSize: Codable, Equatable {
let width: Int
let height: Int
}
/// OSS
struct MediaAlbumUploadItem: Codable, Equatable {
let originalName: String
let ossUrl: String
let size: Int64
let fileWidthSize: MediaAlbumFileSize
/// 线
enum CodingKeys: String, CodingKey {
case originalName = "original_name"
case ossUrl = "oss_url"
case size
case fileWidthSize = "file_width_size"
}
}
///
struct MediaAlbumUploadRequest: Encodable, Equatable {
let name: String
let type: Int
let mediaType: Int
let mediaList: [MediaAlbumUploadItem]
let coverUrl: String
let coverSize: MediaAlbumFileSize
let scenicSpotId: Int
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"
}
}
///
struct MediaAlbumEditRequest: Encodable, Equatable {
let id: Int
let name: String
let type: Int
let mediaType: Int
let mediaList: [MediaAlbumUploadItem]
let coverUrl: String
let coverSize: MediaAlbumFileSize
let scenicSpotId: Int
let description: String
let materialTag: String
/// 线
enum CodingKeys: String, CodingKey {
case id
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"
}
}
///
struct MediaAlbumOperationRequest: Encodable, Equatable {
let id: Int
let listingStatus: Int
/// 线
enum CodingKeys: String, CodingKey {
case id
case listingStatus = "listing_status"
}
}
///
struct MediaAlbumDeleteRequest: Encodable, Equatable {
let id: Int
}
///
struct MediaAlbumAddTagRequest: Encodable, Equatable {
let name: String
}
///
struct MediaLocalUploadFile: Identifiable, Equatable {
let id = UUID()
let data: Data
let fileName: String
let fileType: Int
let width: Int
let height: Int
}
private extension KeyedDecodingContainer {
/// String Bool
func decodeLossyString(forKey key: Key) throws -> String {
if let value = try? decodeIfPresent(String.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value ? "1" : "0"
}
return ""
}
/// StringDouble Int Int
func decodeLossyInt(forKey key: Key) throws -> Int? {
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return Int(value)
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
return Int(value.trimmingCharacters(in: .whitespacesAndNewlines))
}
return nil
}
/// StringDouble Int Int64
func decodeLossyInt64(forKey key: Key) throws -> Int64? {
if let value = try? decodeIfPresent(Int64.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return Int64(value)
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return Int64(value)
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
return Int64(value.trimmingCharacters(in: .whitespacesAndNewlines))
}
return nil
}
/// StringInt Double Double
func decodeLossyDouble(forKey key: Key) throws -> Double? {
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return Double(value)
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
return Double(value.trimmingCharacters(in: .whitespacesAndNewlines))
}
return nil
}
/// StringInt Bool Bool
func decodeLossyBool(forKey key: Key) throws -> Bool? {
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value != 0
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if ["1", "true", "yes"].contains(text) { return true }
if ["0", "false", "no"].contains(text) { return false }
}
return nil
}
}