Add ScenicPermission module and AMap CocoaPods with simulator support.
Integrate scenic selection and permission flows into home routing, add AMap SDK for device builds while keeping arm64 simulators compilable via Podfile post_install hooks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,336 @@
|
||||
//
|
||||
// ScenicPermissionModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 景区申请地区节点实体,表示省市区树中的一个可选节点。
|
||||
struct ScenicAreaNode: Decodable, Equatable, Identifiable {
|
||||
let id: String
|
||||
let code: String
|
||||
let name: String
|
||||
let children: [ScenicAreaNode]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case code
|
||||
case name
|
||||
case children
|
||||
}
|
||||
|
||||
/// 创建地区节点,主要用于单元测试和本地构造。
|
||||
init(id: String, code: String = "", name: String, children: [ScenicAreaNode] = []) {
|
||||
self.id = id
|
||||
self.code = code
|
||||
self.name = name
|
||||
self.children = children
|
||||
}
|
||||
|
||||
/// 自定义解码,兼容后端 id/code 为数字或字符串。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
code = try container.decodeLossyString(forKey: .code)
|
||||
let rawId = try container.decodeLossyString(forKey: .id).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
id = rawId.isEmpty ? code : rawId
|
||||
name = try container.decodeLossyString(forKey: .name)
|
||||
children = try container.decodeIfPresent([ScenicAreaNode].self, forKey: .children) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区申请待审核列表响应实体,表示当前用户提交过的景区入驻申请。
|
||||
struct ScenicApplicationPendingsResponse: Decodable, Equatable {
|
||||
let items: [ScenicApplicationPendingResponse]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items
|
||||
case list
|
||||
case data
|
||||
}
|
||||
|
||||
/// 创建待审核列表响应实体,主要用于测试替身。
|
||||
init(items: [ScenicApplicationPendingResponse] = []) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
/// 自定义解码,兼容后端返回 items/list/data 三种列表字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
items = try container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .items)
|
||||
?? container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .list)
|
||||
?? container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .data)
|
||||
?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区申请待审核实体,表示新增景区申请的审核状态和表单快照。
|
||||
struct ScenicApplicationPendingResponse: Decodable, Equatable, Identifiable {
|
||||
let id: Int
|
||||
let code: String
|
||||
let scenicId: Int
|
||||
let scenicName: String
|
||||
let scenicImages: String
|
||||
let scenicProvince: String
|
||||
let scenicCity: String
|
||||
let coopType: Int
|
||||
let remark: String?
|
||||
let status: Int
|
||||
let rejectReason: String?
|
||||
let auditedBy: String?
|
||||
let auditedAt: String?
|
||||
let auditNote: String?
|
||||
let createdAt: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case code
|
||||
case scenicId = "scenic_id"
|
||||
case scenicName = "scenic_name"
|
||||
case scenicImages = "scenic_images"
|
||||
case scenicProvince = "scenic_province"
|
||||
case scenicCity = "scenic_city"
|
||||
case coopType = "coop_type"
|
||||
case remark
|
||||
case status
|
||||
case rejectReason = "reject_reason"
|
||||
case auditedBy = "audited_by"
|
||||
case auditedAt = "audited_at"
|
||||
case auditNote = "audit_note"
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
|
||||
/// 创建景区申请待审核实体,主要用于测试和本地回填。
|
||||
init(
|
||||
id: Int,
|
||||
code: String = "",
|
||||
scenicId: Int = 0,
|
||||
scenicName: String,
|
||||
scenicImages: String = "",
|
||||
scenicProvince: String = "",
|
||||
scenicCity: String = "",
|
||||
coopType: Int = 1,
|
||||
remark: String? = nil,
|
||||
status: Int = 1,
|
||||
rejectReason: String? = nil,
|
||||
auditedBy: String? = nil,
|
||||
auditedAt: String? = nil,
|
||||
auditNote: String? = nil,
|
||||
createdAt: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.code = code
|
||||
self.scenicId = scenicId
|
||||
self.scenicName = scenicName
|
||||
self.scenicImages = scenicImages
|
||||
self.scenicProvince = scenicProvince
|
||||
self.scenicCity = scenicCity
|
||||
self.coopType = coopType
|
||||
self.remark = remark
|
||||
self.status = status
|
||||
self.rejectReason = rejectReason
|
||||
self.auditedBy = auditedBy
|
||||
self.auditedAt = auditedAt
|
||||
self.auditNote = auditNote
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
/// 自定义解码,兼容后端数字和字符串混用。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
code = try container.decodeLossyString(forKey: .code)
|
||||
scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0
|
||||
scenicName = try container.decodeLossyString(forKey: .scenicName)
|
||||
if let images = try? container.decode([String].self, forKey: .scenicImages) {
|
||||
scenicImages = images.joined(separator: ",")
|
||||
} else {
|
||||
scenicImages = try container.decodeLossyString(forKey: .scenicImages)
|
||||
}
|
||||
scenicProvince = try container.decodeLossyString(forKey: .scenicProvince)
|
||||
scenicCity = try container.decodeLossyString(forKey: .scenicCity)
|
||||
coopType = try container.decodeLossyInt(forKey: .coopType) ?? 1
|
||||
remark = try? container.decodeIfPresent(String.self, forKey: .remark)
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
rejectReason = try? container.decodeIfPresent(String.self, forKey: .rejectReason)
|
||||
auditedBy = try? container.decodeIfPresent(String.self, forKey: .auditedBy)
|
||||
auditedAt = try? container.decodeIfPresent(String.self, forKey: .auditedAt)
|
||||
auditNote = try? container.decodeIfPresent(String.self, forKey: .auditNote)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
/// 景区申请提交请求实体,表示新增景区入驻申请表单。
|
||||
struct ScenicApplicationSubmitRequest: Encodable, Equatable {
|
||||
let scenicName: String?
|
||||
let scenicImages: [String]?
|
||||
let scenicProvince: String
|
||||
let scenicCity: String
|
||||
let coopType: Int
|
||||
let remark: String
|
||||
let scenicId: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicName = "scenic_name"
|
||||
case scenicImages = "scenic_images"
|
||||
case scenicProvince = "scenic_province"
|
||||
case scenicCity = "scenic_city"
|
||||
case coopType = "coop_type"
|
||||
case remark
|
||||
case scenicId = "scenic_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色权限申请实体,表示一条角色/景区权限申请状态。
|
||||
struct RoleApplyPendingResponse: Decodable, Equatable, Hashable, Identifiable {
|
||||
let id: Int
|
||||
let code: String
|
||||
let roleId: Int
|
||||
let roleName: String
|
||||
let scenicList: [RoleApplyScenicItem]
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let createdAt: String
|
||||
let auditedBy: String?
|
||||
let auditedAt: String?
|
||||
let auditNote: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case code
|
||||
case roleId = "role_id"
|
||||
case roleName = "role_name"
|
||||
case scenicList = "scenic_list"
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case createdAt = "created_at"
|
||||
case auditedBy = "audited_by"
|
||||
case auditedAt = "audited_at"
|
||||
case auditNote = "audit_note"
|
||||
}
|
||||
|
||||
/// 创建角色权限申请实体,主要用于测试和状态页回填。
|
||||
init(
|
||||
id: Int,
|
||||
code: String = "",
|
||||
roleId: Int,
|
||||
roleName: String,
|
||||
scenicList: [RoleApplyScenicItem] = [],
|
||||
status: Int = 1,
|
||||
statusLabel: String = "审核中",
|
||||
createdAt: String = "",
|
||||
auditedBy: String? = nil,
|
||||
auditedAt: String? = nil,
|
||||
auditNote: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.code = code
|
||||
self.roleId = roleId
|
||||
self.roleName = roleName
|
||||
self.scenicList = scenicList
|
||||
self.status = status
|
||||
self.statusLabel = statusLabel
|
||||
self.createdAt = createdAt
|
||||
self.auditedBy = auditedBy
|
||||
self.auditedAt = auditedAt
|
||||
self.auditNote = auditNote
|
||||
}
|
||||
|
||||
/// 自定义解码,兼容后端字段类型不稳定。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
code = try container.decodeLossyString(forKey: .code)
|
||||
roleId = try container.decodeLossyInt(forKey: .roleId) ?? 0
|
||||
roleName = try container.decodeLossyString(forKey: .roleName)
|
||||
scenicList = try container.decodeIfPresent([RoleApplyScenicItem].self, forKey: .scenicList) ?? []
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
statusLabel = try container.decodeLossyString(forKey: .statusLabel)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
auditedBy = try? container.decodeIfPresent(String.self, forKey: .auditedBy)
|
||||
auditedAt = try? container.decodeIfPresent(String.self, forKey: .auditedAt)
|
||||
auditNote = try? container.decodeIfPresent(String.self, forKey: .auditNote)
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色权限申请景区实体,表示申请中勾选的一个景区。
|
||||
struct RoleApplyScenicItem: Decodable, Equatable, Hashable, Identifiable {
|
||||
let id: Int
|
||||
let name: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
}
|
||||
|
||||
/// 创建角色权限申请景区实体,主要用于测试。
|
||||
init(id: Int, name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
}
|
||||
|
||||
/// 自定义解码,兼容景区 ID 类型不稳定。
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 角色权限提交请求实体,表示申请某角色在多个景区的权限。
|
||||
struct RoleApplySubmitRequest: Encodable, Equatable {
|
||||
let scenicId: [Int]
|
||||
let roleId: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case roleId = "role_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传占位请求实体,表示景区申请已上传文件的轻量元信息。
|
||||
struct ScenicApplicationUploadPlaceholder: Encodable, Equatable {
|
||||
let fileName: String
|
||||
let fileType: String
|
||||
let fileSize: Int64
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case fileName = "file_name"
|
||||
case fileType = "file_type"
|
||||
case fileSize = "file_size"
|
||||
}
|
||||
}
|
||||
|
||||
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 ? "1" : "0"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 宽松解码整数,兼容后端字符串和浮点数。
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user