Add scenic selection and cooperation order flows aligned with Android.

Upgrade scenic picker with search, location sorting, permission apply/status, and blocking home dialog; add cooperation order module and order source picker improvements with unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 10:48:43 +08:00
parent 3c4ca7eefb
commit 2bea05b1d9
50 changed files with 5603 additions and 191 deletions

View File

@ -0,0 +1,248 @@
//
// CooperationOrderModels.swift
// suixinkan
//
import Foundation
/// Android `CooperationOrderFeature`
enum CooperationOrderFeature {
static let uri = "cooperation_order"
static let remarkNameMaxLength = 10
static func remarkNameValidationMessage(_ remark: String) -> String? {
let trimmed = remark.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.count > remarkNameMaxLength {
return "备注名不能超过\(remarkNameMaxLength)个字符"
}
return nil
}
static func hasPermission(in permissions: [HomePermissionItem]) -> Bool {
permissions.contains { $0.uri == uri }
}
}
/// GET /photog/sale-user/referral-leads 线
struct ReferralLeadEntity: Decodable, Equatable {
let id: Int
let leadId: Int
let displayName: String
let photogRemarkName: String
let name: String
let salerName: String
let userPhone: String
let userMobileMasked: String
let phone: String
let remark: String
let createdAt: String
let imageUrls: [String]
let images: [String]
var displayId: String { String(leadId > 0 ? leadId : id) }
var displayPhone: String {
if !userMobileMasked.isEmpty { return userMobileMasked }
if !userPhone.isEmpty { return userPhone }
return phone
}
var displayImages: [String] { imageUrls.isEmpty ? images : imageUrls }
var displaySalerName: String {
if !displayName.isEmpty { return displayName }
return OrderDisplayName.saleUser(remark: photogRemarkName, primary: name, fallback: salerName)
}
enum CodingKeys: String, CodingKey {
case id
case leadId = "lead_id"
case displayName = "display_name"
case photogRemarkName = "photog_remark_name"
case name
case salerName = "saler_name"
case userPhone = "user_phone"
case userMobileMasked = "user_mobile_masked"
case phone
case remark
case createdAt = "created_at"
case imageUrls = "image_urls"
case images
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
leadId = try container.decodeIfPresent(Int.self, forKey: .leadId) ?? 0
displayName = try container.decodeIfPresent(String.self, forKey: .displayName) ?? ""
photogRemarkName = try container.decodeIfPresent(String.self, forKey: .photogRemarkName) ?? ""
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
salerName = try container.decodeIfPresent(String.self, forKey: .salerName) ?? ""
userPhone = try container.decodeIfPresent(String.self, forKey: .userPhone) ?? ""
userMobileMasked = try container.decodeIfPresent(String.self, forKey: .userMobileMasked) ?? ""
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
remark = try container.decodeIfPresent(String.self, forKey: .remark) ?? ""
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
}
}
/// GET /photog/sale-user/acquisition-orders
struct AcquisitionOrderEntity: Decodable, Equatable {
let id: Int
let orderNumber: String
let projectName: String
let orderType: Int
let orderTypeLabel: String
let orderAmount: String
let amount: String
let actualPayAmount: String
let createdAt: String
let customerPhone: String
let userMobileMasked: String
let phone: String
let saleUserName: String
let photogRemarkName: String
let partnerName: String
var displayAmount: String {
if !orderAmount.isEmpty { return orderAmount }
if !amount.isEmpty { return amount }
return actualPayAmount
}
var displayCustomerPhone: String {
if !userMobileMasked.isEmpty { return userMobileMasked }
if !customerPhone.isEmpty { return customerPhone }
return phone
}
var displayPartner: String {
let salerDisplay = OrderDisplayName.saleUser(remark: photogRemarkName, primary: saleUserName)
if !salerDisplay.isEmpty { return salerDisplay }
return partnerName
}
enum CodingKeys: String, CodingKey {
case id
case orderNumber = "order_number"
case projectName = "project_name"
case orderType = "order_type"
case orderTypeLabel = "order_type_label"
case orderAmount = "order_amount"
case amount
case actualPayAmount = "actual_pay_amount"
case createdAt = "created_at"
case customerPhone = "customer_phone"
case userMobileMasked = "user_mobile_masked"
case phone
case saleUserName = "sale_user_name"
case photogRemarkName = "photog_remark_name"
case partnerName = "partner_name"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeIfPresent(Int.self, forKey: .id) ?? 0
orderNumber = try container.decodeIfPresent(String.self, forKey: .orderNumber) ?? ""
projectName = try container.decodeIfPresent(String.self, forKey: .projectName) ?? ""
orderType = try container.decodeIfPresent(Int.self, forKey: .orderType) ?? 0
orderTypeLabel = try container.decodeIfPresent(String.self, forKey: .orderTypeLabel) ?? ""
orderAmount = try container.decodeIfPresent(String.self, forKey: .orderAmount) ?? ""
amount = try container.decodeIfPresent(String.self, forKey: .amount) ?? ""
actualPayAmount = try container.decodeIfPresent(String.self, forKey: .actualPayAmount) ?? ""
createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? ""
customerPhone = try container.decodeIfPresent(String.self, forKey: .customerPhone) ?? ""
userMobileMasked = try container.decodeIfPresent(String.self, forKey: .userMobileMasked) ?? ""
phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? ""
saleUserName = try container.decodeIfPresent(String.self, forKey: .saleUserName) ?? ""
photogRemarkName = try container.decodeIfPresent(String.self, forKey: .photogRemarkName) ?? ""
partnerName = try container.decodeIfPresent(String.self, forKey: .partnerName) ?? ""
}
}
/// GET /photog/sale-user/invite-info
struct SaleUserInviteInfoResponse: Decodable, Equatable {
let saleUserId: Int
let isBound: Bool
let photographerName: String
let photographerPhone: String
let salerName: String
let salerPhone: String
let shareCode: String
let subtitle: String
let title: String
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case isBound = "is_bound"
case photographerName = "photographer_name"
case photographerPhone = "photographer_phone"
case salerName = "saler_name"
case salerPhone = "saler_phone"
case shareCode = "share_code"
case subtitle
case title
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
saleUserId = try container.decodeIfPresent(Int.self, forKey: .saleUserId) ?? 0
isBound = try container.decodeIfPresent(Bool.self, forKey: .isBound) ?? false
photographerName = try container.decodeIfPresent(String.self, forKey: .photographerName) ?? ""
photographerPhone = try container.decodeIfPresent(String.self, forKey: .photographerPhone) ?? ""
salerName = try container.decodeIfPresent(String.self, forKey: .salerName) ?? ""
salerPhone = try container.decodeIfPresent(String.self, forKey: .salerPhone) ?? ""
shareCode = try container.decodeIfPresent(String.self, forKey: .shareCode) ?? ""
subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle) ?? ""
title = try container.decodeIfPresent(String.self, forKey: .title) ?? ""
}
init(
saleUserId: Int = 0,
isBound: Bool = false,
photographerName: String = "",
photographerPhone: String = "",
salerName: String = "",
salerPhone: String = "",
shareCode: String = "",
subtitle: String = "",
title: String = ""
) {
self.saleUserId = saleUserId
self.isBound = isBound
self.photographerName = photographerName
self.photographerPhone = photographerPhone
self.salerName = salerName
self.salerPhone = salerPhone
self.shareCode = shareCode
self.subtitle = subtitle
self.title = title
}
}
struct SaleUserUpdateRemarkRequest: Encodable {
let saleUserId: Int
let photogRemarkName: String
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case photogRemarkName = "photog_remark_name"
}
}
struct SaleUserCommissionRateSmsRequest: Encodable {
let phone: String
}
struct SaleUserCommissionRateUpdateRequest: Encodable {
let saleUserId: Int
let commissionRate: Int
let smsCode: String
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case commissionRate = "commission_rate"
case smsCode = "sms_code"
}
}