新增景区权限模块,集成 AMap CocoaPods 并支持模拟器
将景区选择与权限申请流程接入首页路由,集成高德 SDK 用于真机构建,并通过 Podfile post_install 钩子保证 arm64 模拟器可编译。 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