Add Projects, Schedule, and Invite modules with home routing.

Migrate project management, schedule management, and photographer invitation flows from placeholders, including project image OSS upload and shared business models.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 15:57:15 +08:00
parent 0d4b63d273
commit fb8430889b
40 changed files with 4496 additions and 108 deletions

View File

@ -0,0 +1,440 @@
//
// ProjectModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import Foundation
///
struct PhotographerProjectDetailResponse: Decodable, Identifiable, Hashable {
let id: Int
let scenicId: Int
let cover: String
let coverProject: String
let coverCarousel: [String]
let name: String
let type: Int
let typeName: String
let status: Int
let statusName: String
let price: String
let otPrice: String
let priceDeposit: String
let description: String
let attrLabel: [String]
let projectContent: String
let sold: Int
let unitName: String
let materialNum: Int
let photoNum: Int
let videoNum: Int
let createdAt: String
let updatedAt: String
let creatorName: String
let creatorPhone: String
let creatorRoleName: String
let auditRemark: String
let photogList: [ProjectPhotographerBrief]
let scenicList: [ProjectScenicSpotBrief]
/// 线
enum CodingKeys: String, CodingKey {
case id
case scenicId = "scenic_id"
case cover
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
case name
case type
case typeName = "type_name"
case status
case statusName = "status_name"
case price
case otPrice = "ot_price"
case priceDeposit = "price_deposit"
case description
case attrLabel = "attr_label"
case projectContent = "project_content"
case sold
case unitName = "unit_name"
case materialNum = "material_num"
case photoNum = "photo_num"
case videoNum = "video_num"
case createdAt = "created_at"
case updatedAt = "updated_at"
case creatorName = "creator_name"
case creatorPhone = "creator_phone"
case creatorRoleName = "creator_role_name"
case auditRemark = "audit_remark"
case photogList = "photog_list"
case scenicList = "scenic_list"
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
scenicId = try container.decodeProjectLossyInt(forKey: .scenicId) ?? 0
cover = try container.decodeProjectLossyString(forKey: .cover)
coverProject = try container.decodeProjectLossyString(forKey: .coverProject)
coverCarousel = try container.decodeProjectLossyStringArray(forKey: .coverCarousel)
name = try container.decodeProjectLossyString(forKey: .name)
type = try container.decodeProjectLossyInt(forKey: .type) ?? 0
typeName = try container.decodeProjectLossyString(forKey: .typeName)
status = try container.decodeProjectLossyInt(forKey: .status) ?? 0
statusName = try container.decodeProjectLossyString(forKey: .statusName)
price = try container.decodeProjectLossyString(forKey: .price)
otPrice = try container.decodeProjectLossyString(forKey: .otPrice)
priceDeposit = try container.decodeProjectLossyString(forKey: .priceDeposit)
description = try container.decodeProjectLossyString(forKey: .description)
attrLabel = try container.decodeProjectLossyStringArray(forKey: .attrLabel)
projectContent = try container.decodeProjectLossyString(forKey: .projectContent)
sold = try container.decodeProjectLossyInt(forKey: .sold) ?? 0
unitName = try container.decodeProjectLossyString(forKey: .unitName)
materialNum = try container.decodeProjectLossyInt(forKey: .materialNum) ?? 0
photoNum = try container.decodeProjectLossyInt(forKey: .photoNum) ?? 0
videoNum = try container.decodeProjectLossyInt(forKey: .videoNum) ?? 0
createdAt = try container.decodeProjectLossyString(forKey: .createdAt)
updatedAt = try container.decodeProjectLossyString(forKey: .updatedAt)
creatorName = try container.decodeProjectLossyString(forKey: .creatorName)
creatorPhone = try container.decodeProjectLossyString(forKey: .creatorPhone)
creatorRoleName = try container.decodeProjectLossyString(forKey: .creatorRoleName)
auditRemark = try container.decodeProjectLossyString(forKey: .auditRemark)
photogList = (try? container.decode([ProjectPhotographerBrief].self, forKey: .photogList)) ?? []
scenicList = (try? container.decode([ProjectScenicSpotBrief].self, forKey: .scenicList)) ?? []
}
}
///
struct ProjectPhotographerBrief: Decodable, Identifiable, Hashable {
let id: Int
let photogUid: Int
let nickname: String
let avatar: String
let orderNum: Int
let completedOrderCount: Int
/// 线
enum CodingKeys: String, CodingKey {
case id
case photogUid = "photog_uid"
case nickname
case avatar
case orderNum = "order_num"
case completedOrderCount = "completed_order_count"
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
photogUid = try container.decodeProjectLossyInt(forKey: .photogUid) ?? 0
nickname = try container.decodeProjectLossyString(forKey: .nickname)
avatar = try container.decodeProjectLossyString(forKey: .avatar)
orderNum = try container.decodeProjectLossyInt(forKey: .orderNum) ?? 0
completedOrderCount = try container.decodeProjectLossyInt(forKey: .completedOrderCount) ?? 0
}
}
///
struct ProjectScenicSpotBrief: Decodable, Identifiable, Hashable {
let id: Int
let name: String
///
enum CodingKeys: String, CodingKey {
case id
case name
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
name = try container.decodeProjectLossyString(forKey: .name)
}
}
///
struct ProjectCreateRequest: Encodable, Equatable {
let id: Int?
let type: Int
let scenicId: Int
let name: String
let price: String
let otPrice: String?
let coverProject: String
let coverCarousel: [String]?
let description: String
let attrLabel: [String]?
let extra: ProjectCreateExtra
let allowNFC: Int = 1
/// 线
enum CodingKeys: String, CodingKey {
case id
case type
case scenicId = "scenic_id"
case name
case price
case otPrice = "ot_price"
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
case description
case attrLabel = "attr_label"
case extra
case allowNFC = "allow_nfc"
}
}
/// /
struct ProjectCreateExtra: Encodable, Equatable {
let priceDeposit: String
let materialNum: Int
let photoNum: Int
let videoNum: Int
let scenicSpotId: [Int]
let photogUid: [Int]
/// 线
enum CodingKeys: String, CodingKey {
case priceDeposit = "price_deposit"
case materialNum = "material_num"
case photoNum = "photo_num"
case videoNum = "video_num"
case scenicSpotId = "scenic_spot_id"
case photogUid = "photog_uid"
}
}
///
struct ProjectDeleteRequest: Encodable, Equatable {
let id: Int
}
///
struct StoreManagerScenicItem: Decodable, Identifiable, Hashable {
let id: Int
let name: String
///
enum CodingKeys: String, CodingKey {
case id
case name
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
name = try container.decodeProjectLossyString(forKey: .name)
}
}
///
struct StoreManagerPackageItem: Codable, Hashable {
let materialNum: Int
let price: String
/// 线
enum CodingKeys: String, CodingKey {
case materialNum = "material_num"
case price
}
}
///
struct StoreManagerCreateRequest: Encodable, Equatable {
let name: String
let storeId: Int?
let type: Int
let description: String
let coverProject: String?
let coverCarousel: [String]?
let projectRule: String?
let scenicId: [Int]
let settleSpotNum: Int
let price: Double
let priceMaterial: Double
let pricePhoto: Double
let priceVideo: Double
let priceMaterialAll: Double?
let packageList: [StoreManagerPackageItem]?
let userId: Int
let singleSpotMaterialNum: Int
let singleSpotPhotoNum: Int
let singleSpotVideoNum: Int
/// 线
enum CodingKeys: String, CodingKey {
case name
case storeId = "store_id"
case type
case description
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
case projectRule = "project_rule"
case scenicId = "scenic_id"
case settleSpotNum = "settle_spot_num"
case price
case priceMaterial = "price_material"
case pricePhoto = "price_photo"
case priceVideo = "price_video"
case priceMaterialAll = "price_material_all"
case packageList = "package"
case userId = "user_id"
case singleSpotMaterialNum = "single_spot_material_num"
case singleSpotPhotoNum = "single_spot_photo_num"
case singleSpotVideoNum = "single_spot_video_num"
}
}
///
struct StoreManagerUpdateRequest: Encodable, Equatable {
let id: Int
let name: String
let type: Int
let description: String
let coverProject: String?
let coverCarousel: [String]?
let projectRule: String?
let scenicId: [Int]
let settleSpotNum: Int
let price: Double
let priceMaterial: Double
let pricePhoto: Double
let priceVideo: Double
let priceMaterialAll: Double?
let packageList: [StoreManagerPackageItem]?
let userId: Int
let singleSpotMaterialNum: Int
let singleSpotPhotoNum: Int
let singleSpotVideoNum: Int
/// 线
enum CodingKeys: String, CodingKey {
case id
case name
case type
case description
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
case projectRule = "project_rule"
case scenicId = "scenic_id"
case settleSpotNum = "settle_spot_num"
case price
case priceMaterial = "price_material"
case pricePhoto = "price_photo"
case priceVideo = "price_video"
case priceMaterialAll = "price_material_all"
case packageList = "package"
case userId = "user_id"
case singleSpotMaterialNum = "single_spot_material_num"
case singleSpotPhotoNum = "single_spot_photo_num"
case singleSpotVideoNum = "single_spot_video_num"
}
}
///
struct StoreManagerOfflineCreateRequest: Encodable, Equatable {
let name: String
let scenicId: Int
let storeId: Int
let description: String
let price: Double
let coverProject: String
let coverCarousel: [String]
/// 线
enum CodingKeys: String, CodingKey {
case name
case scenicId = "scenic_id"
case storeId = "store_id"
case description
case price
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
}
}
///
struct StoreManagerOfflineUpdateRequest: Encodable, Equatable {
let id: Int
let name: String
let scenicId: Int
let description: String
let price: Double
let coverProject: String?
let coverCarousel: [String]?
let storeId: Int?
/// 线
enum CodingKeys: String, CodingKey {
case id
case name
case scenicId = "scenic_id"
case description
case price
case coverProject = "cover_project"
case coverCarousel = "cover_carousel"
case storeId = "store_id"
}
}
/// PhotosPicker
struct ProjectLocalImage: Identifiable, Equatable {
let id = UUID()
let data: Data
let fileName: String
let previewURL: String?
}
private extension KeyedDecodingContainer {
/// String Bool
func decodeProjectLossyString(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 ""
}
/// StringDouble Int Int
func decodeProjectLossyInt(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
}
///
func decodeProjectLossyStringArray(forKey key: Key) throws -> [String] {
if let values = try? decodeIfPresent([String].self, forKey: key) {
return values
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
return value
.components(separatedBy: CharacterSet(charactersIn: ",\n"))
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
return []
}
}