feat: 更新素材管理和举报风险地图

This commit is contained in:
2026-07-09 12:20:02 +08:00
parent 9b92d81902
commit 42aca73588
42 changed files with 6164 additions and 1369 deletions

View File

@ -0,0 +1,541 @@
//
// MaterialManagementModels.swift
// suixinkan
//
import Foundation
/// Android `MaterialListViewModel`
enum MaterialFilterStatus: 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 MaterialMediaType: 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 1 2
var uploadFileType: Int {
switch self {
case .image: 2
case .video: 1
}
}
}
/// Android `MaterialItemEntity`
struct MaterialItem: 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
}
}
/// Android `MaterialListResponse`
struct MaterialListResponse: Decodable, Sendable, Equatable {
let total: Int
let list: [MaterialItem]
let order: MaterialOrderInfo?
init(total: Int = 0, list: [MaterialItem] = [], order: MaterialOrderInfo? = nil) {
self.total = total
self.list = list
self.order = order
}
}
/// Android `MaterialOrderInfo`
struct MaterialOrderInfo: Decodable, Sendable, Equatable, Hashable {
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 `MaterialDetailResponse`
struct MaterialDetail: Decodable, Sendable, Equatable {
let id: Int
let name: String?
let type: String?
let ossUrl: String?
let coverUrl: String?
let coverSize: Int?
let description: String?
let scenicSpotId: Int64?
let scenicAreaId: Int?
let materialTag: [MaterialTagItem]?
let uploaderId: Int?
let status: Int
let likesCount: Int
let downloadCount: Int
let collectCount: Int
let isRecommended: Bool
let createdAt: String?
let updatedAt: String?
let deletedAt: String?
let listingStatus: Int
let reviewNotes: String?
let reviewTime: String?
let reviewerId: Int?
let reviewer: String?
let provinceId: Int?
let cityId: Int?
let shareCount: Int
let isWeb: Int
let isMini: Int
let uploaderName: String?
let uploaderAvatar: String?
let uploaderDescription: String?
let platformCertification: Bool
let scenicCertification: Bool
let mediaList: [MaterialDetailMediaItem]?
enum CodingKeys: String, CodingKey {
case id
case name
case type
case ossUrl = "oss_url"
case coverUrl = "cover_url"
case coverSize = "cover_size"
case description
case scenicSpotId = "scenic_spot_id"
case scenicAreaId = "scenic_area_id"
case materialTag = "material_tag"
case uploaderId = "uploader_id"
case status
case likesCount = "likes_count"
case downloadCount = "download_count"
case collectCount = "collect_count"
case isRecommended = "is_recommended"
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 reviewer
case provinceId = "province_id"
case cityId = "city_id"
case shareCount = "share_count"
case isWeb = "is_web"
case isMini = "is_mini"
case uploaderName = "uploader_name"
case uploaderAvatar = "uploader_avatar"
case uploaderDescription = "uploader_description"
case platformCertification = "platform_certification"
case scenicCertification = "scenic_certification"
case mediaList = "media_list"
}
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.decodeFlexibleString(forKey: .type)
ossUrl = try container.decodeIfPresent(String.self, forKey: .ossUrl)
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.decodeFlexibleInt64(forKey: .scenicSpotId)
scenicAreaId = try container.decodeIfPresent(Int.self, forKey: .scenicAreaId)
materialTag = try container.decodeIfPresent([MaterialTagItem].self, forKey: .materialTag)
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
isRecommended = try container.decodeIfPresent(Bool.self, forKey: .isRecommended) ?? false
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)
reviewer = try container.decodeIfPresent(String.self, forKey: .reviewer)
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
isWeb = try container.decodeIfPresent(Int.self, forKey: .isWeb) ?? 0
isMini = try container.decodeIfPresent(Int.self, forKey: .isMini) ?? 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([MaterialDetailMediaItem].self, forKey: .mediaList)
}
}
///
struct MaterialTagItem: Codable, Sendable, Equatable, Hashable, Identifiable {
let id: Int
let name: String
}
/// Android `MaterialDetailMediaItem`
struct MaterialDetailMediaItem: 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) ?? 1
size = try container.decodeIfPresent(Int64.self, forKey: .size)
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).sxkNonEmpty)
?? (showUrl?.trimmingCharacters(in: .whitespacesAndNewlines).sxkNonEmpty)
?? ""
}
}
/// Android `UploadMaterialRequest` / `EditMaterialRequest`
struct MaterialUploadRequest: Encodable, Sendable, Equatable {
let id: Int?
let name: String
let type: String
let mediaType: String
let mediaList: [MaterialUploadMediaItem]
let coverUrl: String
let coverSize: String
let scenicSpotId: Int64
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"
}
init(
id: Int? = nil,
name: String,
mediaType: MaterialMediaType,
mediaList: [MaterialUploadMediaItem],
coverUrl: String,
coverSize: String,
scenicSpotId: Int64,
materialTag: String
) {
self.id = id
self.name = name
self.type = "1"
self.mediaType = String(mediaType.rawValue)
self.mediaList = mediaList
self.coverUrl = coverUrl
self.coverSize = coverSize
self.scenicSpotId = scenicSpotId
self.description = ""
self.materialTag = materialTag
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(type, forKey: .type)
try container.encode(mediaType, forKey: .mediaType)
try container.encode(mediaList, forKey: .mediaList)
try container.encode(coverUrl, forKey: .coverUrl)
try container.encode(coverSize, forKey: .coverSize)
try container.encode(scenicSpotId, forKey: .scenicSpotId)
try container.encode(description, forKey: .description)
try container.encode(materialTag, forKey: .materialTag)
}
}
/// Android `MediaItem`
struct MaterialUploadMediaItem: Encodable, Sendable, Equatable {
let originalName: String
let ossUrl: String
let size: Int64
let fileWidthSize: MaterialUploadFileSize
enum CodingKeys: String, CodingKey {
case originalName = "original_name"
case ossUrl = "oss_url"
case size
case fileWidthSize = "file_width_size"
}
}
///
struct MaterialUploadFileSize: Encodable, Sendable, Equatable {
let width: Int
let height: Int
}
/// UIKit ViewModel
struct MaterialUploadedMediaItem: Sendable, Equatable, Hashable, Identifiable {
let id: String
let data: Data
let thumbnailData: Data?
let previewURL: String?
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,
previewURL: String? = 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.previewURL = previewURL
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 MaterialUploadDialogState: Sendable, Equatable {
let title: String
let progress: Int
}
/// UI
enum MaterialDisplayFormatter {
/// 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 formatNumber(_ number: Int) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: number)) ?? String(number)
}
///
static func reviewStatusText(_ status: Int) -> String {
switch status {
case 1: "已通过"
case 0: "待审核"
case 2: "未通过"
default: "未知"
}
}
}
private extension KeyedDecodingContainer {
func decodeFlexibleString(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)
}
return nil
}
func decodeFlexibleInt64(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(String.self, forKey: key) {
return Int64(value)
}
return nil
}
}
private extension String {
var sxkNonEmpty: String? { isEmpty ? nil : self }
}