新增项目、排期与邀请模块,并接入首页路由
将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
156
suixinkan/Features/Invite/Models/InviteModels.swift
Normal file
156
suixinkan/Features/Invite/Models/InviteModels.swift
Normal file
@ -0,0 +1,156 @@
|
||||
//
|
||||
// InviteModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 邀请信息响应实体,表示邀请码、邀请链接和规则文案。
|
||||
struct InviteInfoResponse: Decodable, Equatable {
|
||||
let enableInvite: Bool
|
||||
let inviteCode: String
|
||||
let inviteUrl: String
|
||||
let description: [String]
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case enableInvite = "enable_invite"
|
||||
case inviteCode = "invite_code"
|
||||
case inviteUrl = "invite_url"
|
||||
case description
|
||||
}
|
||||
|
||||
/// 宽松解码邀请信息。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
enableInvite = try container.decodeInviteLossyBool(forKey: .enableInvite) ?? true
|
||||
inviteCode = try container.decodeInviteLossyString(forKey: .inviteCode)
|
||||
inviteUrl = try container.decodeInviteLossyString(forKey: .inviteUrl)
|
||||
description = (try? container.decodeIfPresent([String].self, forKey: .description)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请用户实体,表示邀请记录里的摄影师。
|
||||
struct InviteUserItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let realName: String
|
||||
let phone: String
|
||||
let avatar: String
|
||||
let createdAt: String
|
||||
let inviteLevel: Int
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case realName = "real_name"
|
||||
case phone
|
||||
case avatar
|
||||
case createdAt = "created_at"
|
||||
case inviteLevel = "invite_level"
|
||||
}
|
||||
|
||||
/// 宽松解码邀请用户字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeInviteLossyInt(forKey: .id) ?? 0
|
||||
realName = try container.decodeInviteLossyString(forKey: .realName)
|
||||
phone = try container.decodeInviteLossyString(forKey: .phone)
|
||||
avatar = try container.decodeInviteLossyString(forKey: .avatar)
|
||||
createdAt = try container.decodeInviteLossyString(forKey: .createdAt)
|
||||
inviteLevel = try container.decodeInviteLossyInt(forKey: .inviteLevel) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请记录页面展示行实体,统一邀请用户和奖励明细的展示字段。
|
||||
struct InviteDisplayRow: Identifiable, Equatable {
|
||||
let id: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let amount: String
|
||||
let time: String
|
||||
let phone: String
|
||||
let avatar: String
|
||||
let inviteLevel: Int
|
||||
|
||||
/// 邀请层级文案。
|
||||
var levelText: String {
|
||||
inviteLevel == 1 ? "一级" : "二级"
|
||||
}
|
||||
|
||||
/// 头像占位文案。
|
||||
var avatarPlaceholder: String {
|
||||
if let first = title.first {
|
||||
return String(first)
|
||||
}
|
||||
if let first = phone.first {
|
||||
return String(first)
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
}
|
||||
|
||||
/// 邀请记录分段类型。
|
||||
enum InviteRecordTab: CaseIterable, Equatable {
|
||||
case invite
|
||||
case reward
|
||||
|
||||
/// 分段标题。
|
||||
var title: String {
|
||||
switch self {
|
||||
case .invite: return "邀请记录"
|
||||
case .reward: return "奖励记录"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeInviteLossyString(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、Double 和 Int 宽松解码为 Int。
|
||||
func decodeInviteLossyInt(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) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Int(text) ?? Double(text).map(Int.init)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 String、数字和 Bool 宽松解码为 Bool。
|
||||
func decodeInviteLossyBool(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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user