Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,226 @@
|
||||
//
|
||||
// MessageCenterModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// 消息中心列表响应,使用 last_id 游标分页。
|
||||
struct MessageListResponse: Decodable, Equatable {
|
||||
let hasMore: Bool
|
||||
let lastId: Int
|
||||
let items: [MessageEntity]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case hasMore = "has_more"
|
||||
case lastId = "last_id"
|
||||
case items
|
||||
}
|
||||
|
||||
/// 创建空消息响应,主要用于测试和失败兜底。
|
||||
init(hasMore: Bool = false, lastId: Int = 0, items: [MessageEntity] = []) {
|
||||
self.hasMore = hasMore
|
||||
self.lastId = lastId
|
||||
self.items = items
|
||||
}
|
||||
|
||||
/// 宽松解码分页状态,兼容后端数字/字符串/布尔类型不稳定。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
hasMore = try container.decodeLossyBool(forKey: .hasMore) ?? false
|
||||
lastId = try container.decodeLossyInt(forKey: .lastId) ?? 0
|
||||
items = try container.decodeIfPresent([MessageEntity].self, forKey: .items) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 后端消息实体。
|
||||
struct MessageEntity: Decodable, Equatable, Identifiable {
|
||||
let id: Int
|
||||
let type: Int
|
||||
let typeName: String
|
||||
let title: String
|
||||
let content: String
|
||||
let pushAt: String
|
||||
let createdAt: String
|
||||
let isRead: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case typeName = "type_name"
|
||||
case title
|
||||
case content
|
||||
case pushAt = "push_at"
|
||||
case createdAt = "created_at"
|
||||
case isRead = "is_read"
|
||||
}
|
||||
|
||||
/// 创建消息实体,主要用于测试。
|
||||
init(
|
||||
id: Int,
|
||||
type: Int,
|
||||
typeName: String = "",
|
||||
title: String = "",
|
||||
content: String = "",
|
||||
pushAt: String = "",
|
||||
createdAt: String = "",
|
||||
isRead: Bool = false
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.typeName = typeName
|
||||
self.title = title
|
||||
self.content = content
|
||||
self.pushAt = pushAt
|
||||
self.createdAt = createdAt
|
||||
self.isRead = isRead
|
||||
}
|
||||
|
||||
/// 宽松解码消息字段,避免单条异常字段导致整页失败。
|
||||
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
|
||||
typeName = try container.decodeLossyString(forKey: .typeName)
|
||||
title = try container.decodeLossyString(forKey: .title)
|
||||
content = try container.decodeLossyString(forKey: .content)
|
||||
pushAt = try container.decodeLossyString(forKey: .pushAt)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
isRead = try container.decodeLossyBool(forKey: .isRead) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除消息请求体。
|
||||
struct MessageDeleteRequest: Encodable {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
/// 页面展示用消息类型。
|
||||
enum MessageType: Equatable {
|
||||
case order
|
||||
case writeOff
|
||||
case system
|
||||
|
||||
/// 消息类型图标。
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .order:
|
||||
return "cart.fill"
|
||||
case .writeOff:
|
||||
return "qrcode.viewfinder"
|
||||
case .system:
|
||||
return "bell.badge.fill"
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息类型标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .order:
|
||||
return "订单"
|
||||
case .writeOff:
|
||||
return "核销"
|
||||
case .system:
|
||||
return "系统"
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息类型颜色。
|
||||
var color: UIColor {
|
||||
switch self {
|
||||
case .order:
|
||||
return AppDesign.primary
|
||||
case .writeOff:
|
||||
return AppDesign.success
|
||||
case .system:
|
||||
return AppDesign.warning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面展示用消息项。
|
||||
struct MessageItem: Identifiable, Equatable {
|
||||
let id: String
|
||||
let msgId: Int?
|
||||
let title: String
|
||||
let detail: String
|
||||
let time: String
|
||||
var isRead: Bool
|
||||
let type: MessageType
|
||||
}
|
||||
|
||||
/// 消息筛选项。
|
||||
enum MessageFilter: Int, CaseIterable, Identifiable {
|
||||
case all = 0
|
||||
case unread = 1
|
||||
|
||||
var id: Int { rawValue }
|
||||
|
||||
/// 筛选标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "全部"
|
||||
case .unread:
|
||||
return "未读"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
||||
/// 将 String、数字和 Bool 宽松解码为 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),
|
||||
case let text = value.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!text.isEmpty {
|
||||
return Int(text) ?? Int(Double(text) ?? 0)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value ? 1 : 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 String、数字和 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),
|
||||
case let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
||||
!text.isEmpty {
|
||||
if ["1", "true", "yes"].contains(text) { return true }
|
||||
if ["0", "false", "no"].contains(text) { return false }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user