feat: 更新素材管理和举报风险地图
This commit is contained in:
23
suixinkan/Features/Setting/API/SettingAPI.swift
Normal file
23
suixinkan/Features/Setting/API/SettingAPI.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// SettingAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 设置中心 API,封装版本检查等设置页相关接口。
|
||||
final class SettingAPI {
|
||||
private let client: APIClient
|
||||
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 拉取后端配置的最新 App 版本。
|
||||
func checkLatestVersion() async throws -> VersionResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/app/latest-version")
|
||||
)
|
||||
}
|
||||
}
|
||||
136
suixinkan/Features/Setting/Models/SettingModels.swift
Normal file
136
suixinkan/Features/Setting/Models/SettingModels.swift
Normal file
@ -0,0 +1,136 @@
|
||||
//
|
||||
// SettingModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 最新版本响应实体,对齐 Android `VersionResponse`。
|
||||
struct VersionResponse: Decodable, Equatable {
|
||||
let id: Int
|
||||
let type: Int
|
||||
let version: String
|
||||
let operatorName: String
|
||||
let description: String
|
||||
let status: Int
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
let deletedAt: String?
|
||||
let versionCode: Int
|
||||
let apkURL: String
|
||||
let isForceUpdate: Int
|
||||
let approveNotes: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case version
|
||||
case operatorName = "operator"
|
||||
case description
|
||||
case status
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case deletedAt = "deleted_at"
|
||||
case versionCode = "version_code"
|
||||
case apkURL = "apk_url"
|
||||
case isForceUpdate = "is_force_update"
|
||||
case approveNotes = "approve_notes"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
type: Int = 0,
|
||||
version: String = "",
|
||||
operatorName: String = "",
|
||||
description: String = "",
|
||||
status: Int = 0,
|
||||
createdAt: String = "",
|
||||
updatedAt: String = "",
|
||||
deletedAt: String? = nil,
|
||||
versionCode: Int = 0,
|
||||
apkURL: String = "",
|
||||
isForceUpdate: Int = 0,
|
||||
approveNotes: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.version = version
|
||||
self.operatorName = operatorName
|
||||
self.description = description
|
||||
self.status = status
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
self.deletedAt = deletedAt
|
||||
self.versionCode = versionCode
|
||||
self.apkURL = apkURL
|
||||
self.isForceUpdate = isForceUpdate
|
||||
self.approveNotes = approveNotes
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
type = try container.decodeLossyInt(forKey: .type) ?? 0
|
||||
version = try container.decodeLossyString(forKey: .version)
|
||||
operatorName = try container.decodeLossyString(forKey: .operatorName)
|
||||
description = try container.decodeLossyString(forKey: .description)
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
updatedAt = try container.decodeLossyString(forKey: .updatedAt)
|
||||
deletedAt = try container.decodeIfPresent(String.self, forKey: .deletedAt)
|
||||
versionCode = try container.decodeLossyInt(forKey: .versionCode) ?? 0
|
||||
apkURL = try container.decodeLossyString(forKey: .apkURL)
|
||||
isForceUpdate = try container.decodeLossyInt(forKey: .isForceUpdate) ?? 0
|
||||
approveNotes = try container.decodeLossyString(forKey: .approveNotes)
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页协议入口实体,表示标题和对应 H5 地址。
|
||||
struct SettingAgreementDestination: Equatable {
|
||||
let title: String
|
||||
let url: URL
|
||||
}
|
||||
|
||||
/// 设置页协议类型。
|
||||
enum SettingAgreementKind: CaseIterable {
|
||||
case aboutUs
|
||||
case userAgreement
|
||||
case privacyPolicy
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
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 ? "true" : "false"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let intValue = Int(text) {
|
||||
return intValue
|
||||
}
|
||||
if let doubleValue = Double(text) {
|
||||
return Int(doubleValue)
|
||||
}
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
87
suixinkan/Features/Setting/ViewModels/SettingViewModel.swift
Normal file
87
suixinkan/Features/Setting/ViewModels/SettingViewModel.swift
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// SettingViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 设置中心 ViewModel,管理版本展示、协议入口和更新标记。
|
||||
final class SettingViewModel {
|
||||
private let infoDictionary: [String: Any]?
|
||||
private let environment: APIEnvironment
|
||||
|
||||
private(set) var latestVersion: VersionResponse?
|
||||
private(set) var hasNewVersion = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
init(
|
||||
infoDictionary: [String: Any]? = Bundle.main.infoDictionary,
|
||||
environment: APIEnvironment = .current
|
||||
) {
|
||||
self.infoDictionary = infoDictionary
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
var appVersion: String {
|
||||
AppClientInfo.appVersion(infoDictionary: infoDictionary)
|
||||
}
|
||||
|
||||
var appDownloadURL: URL {
|
||||
environment.baseURL.appending(path: "h5/app/download")
|
||||
}
|
||||
|
||||
/// 静默检查最新版本,失败时保持当前页面状态。
|
||||
func checkLatestVersion(api: SettingAPI) async {
|
||||
do {
|
||||
let response = try await api.checkLatestVersion()
|
||||
latestVersion = response
|
||||
hasNewVersion = Self.isNewerVersionCode(response.versionCode, than: currentVersionCode(infoDictionary: infoDictionary))
|
||||
notifyStateChange()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
latestVersion = nil
|
||||
hasNewVersion = false
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回指定协议入口的标题和 URL。
|
||||
func agreementDestination(for kind: SettingAgreementKind) -> SettingAgreementDestination {
|
||||
switch kind {
|
||||
case .aboutUs:
|
||||
SettingAgreementDestination(
|
||||
title: "关于我们",
|
||||
url: environment.baseURL.appending(path: "h5/app/about-us")
|
||||
)
|
||||
case .userAgreement:
|
||||
SettingAgreementDestination(
|
||||
title: "用户协议",
|
||||
url: environment.baseURL.appending(path: "h5/app/user-agreement")
|
||||
)
|
||||
case .privacyPolicy:
|
||||
SettingAgreementDestination(
|
||||
title: "隐私政策",
|
||||
url: environment.baseURL.appending(path: "h5/app/privacy-policy")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断后端版本号是否高于当前构建号。
|
||||
nonisolated static func isNewerVersionCode(_ latestVersionCode: Int, than currentVersionCode: Int?) -> Bool {
|
||||
guard let currentVersionCode else { return false }
|
||||
return latestVersionCode > currentVersionCode
|
||||
}
|
||||
|
||||
private func currentVersionCode(infoDictionary: [String: Any]?) -> Int? {
|
||||
let build = (infoDictionary?["CFBundleVersion"] as? String)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let build, !build.isEmpty else { return nil }
|
||||
return Int(build)
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user