迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。
新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,159 @@
|
||||
//
|
||||
// CooperationOrderAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 合作订单服务协议,抽象获客员与带客单相关接口以便测试替换。
|
||||
protocol CooperationOrderServing {
|
||||
/// 获取合作获客员列表。
|
||||
func cooperativeSalers(storeId: Int?, page: Int, pageSize: Int) async throws -> CooperativeSalerListPayload
|
||||
|
||||
/// 获取获客员邀请信息。
|
||||
func saleUserInviteInfo(saleUserId: Int) async throws -> SaleUserInviteInfo
|
||||
|
||||
/// 接受获客员绑定。
|
||||
func saleUserAcceptBind(saleUserId: Int) async throws
|
||||
|
||||
/// 获取获客员线索列表。
|
||||
func saleUserReferralLeads(page: Int, pageSize: Int) async throws -> ListPayload<ReferralLead>
|
||||
|
||||
/// 获取获客订单列表。
|
||||
func saleUserAcquisitionOrders(keyword: String?, page: Int, pageSize: Int) async throws -> ListPayload<AcquisitionOrder>
|
||||
|
||||
/// 获取可绑定带客单列表。
|
||||
func saleUserReferralOrders(saleUserId: Int, page: Int, pageSize: Int) async throws -> ReferralOrderListPayload
|
||||
|
||||
/// 绑定带客单到订单。
|
||||
func saleUserBindReferralOrder(orderNumber: String, referralOrderNo: String) async throws
|
||||
|
||||
/// 更新带客单状态(如退款时 status=3)。
|
||||
func saleUserUpdateReferralOrderStatus(id: Int, status: Int) async throws
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 合作订单 API,封装合作获客员、线索、获客订单与带客单绑定接口。
|
||||
final class CooperationOrderAPI: CooperationOrderServing {
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化合作订单 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 获取合作获客员列表。
|
||||
func cooperativeSalers(storeId: Int? = nil, page: Int = 1, pageSize: Int = 10) async throws -> CooperativeSalerListPayload {
|
||||
var query = [
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))")
|
||||
]
|
||||
if let storeId, storeId > 0 {
|
||||
query.append(URLQueryItem(name: "store_id", value: "\(storeId)"))
|
||||
}
|
||||
return try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/cooperative-salers",
|
||||
queryItems: query
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取获客员邀请信息。
|
||||
func saleUserInviteInfo(saleUserId: Int) async throws -> SaleUserInviteInfo {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/invite-info",
|
||||
queryItems: [URLQueryItem(name: "sale_user_id", value: "\(saleUserId)")]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 接受获客员绑定。
|
||||
func saleUserAcceptBind(saleUserId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/accept-bind",
|
||||
queryItems: [URLQueryItem(name: "sale_user_id", value: "\(saleUserId)")]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取获客员线索列表。
|
||||
func saleUserReferralLeads(page: Int = 1, pageSize: Int = 10) async throws -> ListPayload<ReferralLead> {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/referral-leads",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))")
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取获客订单列表。
|
||||
func saleUserAcquisitionOrders(keyword: String? = nil, page: Int = 1, pageSize: Int = 10) async throws -> ListPayload<AcquisitionOrder> {
|
||||
var query = [
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))")
|
||||
]
|
||||
if let keyword, !keyword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
query.append(URLQueryItem(name: "keyword", value: keyword))
|
||||
}
|
||||
return try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/acquisition-orders",
|
||||
queryItems: query
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取可绑定带客单列表。
|
||||
func saleUserReferralOrders(saleUserId: Int, page: Int = 1, pageSize: Int = 10) async throws -> ReferralOrderListPayload {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/sale-user/referral-orders",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "sale_user_id", value: "\(saleUserId)"),
|
||||
URLQueryItem(name: "page", value: "\(max(page, 1))"),
|
||||
URLQueryItem(name: "page_size", value: "\(max(pageSize, 1))")
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 绑定带客单到订单。
|
||||
func saleUserBindReferralOrder(orderNumber: String, referralOrderNo: String) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/bind-referral-order",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "order_number", value: orderNumber),
|
||||
URLQueryItem(name: "referral_order_no", value: referralOrderNo)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新带客单状态。
|
||||
func saleUserUpdateReferralOrderStatus(id: Int, status: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/referral-orders/\(id)/status",
|
||||
body: ReferralOrderStatusUpdateRequest(status: status)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
57
suixinkan/Features/CooperationOrder/CooperationOrder.md
Normal file
57
suixinkan/Features/CooperationOrder/CooperationOrder.md
Normal file
@ -0,0 +1,57 @@
|
||||
# 合作订单模块
|
||||
|
||||
## 职责
|
||||
|
||||
合作订单模块面向摄影师/门店角色,管理合作获客业务:
|
||||
|
||||
- **获客员线索**:查看合作获客员上报的潜在客户线索(手机号、备注、图片)
|
||||
- **获客订单**:查看通过合作渠道产生的订单,支持关键词搜索
|
||||
- **合作获客员**:查看已绑定获客员列表,扫码绑定新获客员
|
||||
|
||||
权限 URI:`cooperation_order`(与 Android `CooperationOrderFeature.URI` 一致)。
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 首页入口
|
||||
|
||||
1. 用户点击首页「合作订单」菜单(`cooperation_order`)
|
||||
2. `CooperationOrderAccessView` 校验 `PermissionContext.canAccess("cooperation_order")`
|
||||
3. 无权限时 Toast 提示并返回;有权限进入 `CooperationOrderListView`
|
||||
|
||||
### 双 Tab 列表
|
||||
|
||||
| Tab | API | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 获客员线索 | `GET .../sale-user/referral-leads` | 分页 10 条,下拉刷新/加载更多 |
|
||||
| 获客订单 | `GET .../sale-user/acquisition-orders` | 支持 `keyword` 搜索 |
|
||||
|
||||
### 获客员绑定
|
||||
|
||||
1. 列表页右上角「合作获客员」→ `CooperationAcquirerView`
|
||||
2. 扫码或主 Tab 扫码识别 `{"method":"sale_user_bind","sale_user_id":N}`
|
||||
3. `BindAcquirerView` 加载邀请信息 → 确认 → `POST .../sale-user/accept-bind`
|
||||
|
||||
## 与订单模块关系
|
||||
|
||||
订单管理(`orderType` 11 摄影师跟拍 / 14 线下扫码)在订单卡片展示「订单来源」:
|
||||
|
||||
- Sheet 内选择获客员 → 带客单 → `POST .../bind-referral-order`
|
||||
- 全页流程见 `ReferralOrderSelectView`
|
||||
- 订单退款成功后,若已绑定带客单,调用 `POST .../referral-orders/{id}/status`(status=3)
|
||||
|
||||
## 主要文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `API/CooperationOrderAPI.swift` | 8 个 sale-user / cooperative 接口 |
|
||||
| `Models/CooperationOrderModels.swift` | 实体与解码 |
|
||||
| `Services/CooperationOrderFeature.swift` | 权限 URI 常量 |
|
||||
| `ViewModels/*` | 列表、获客员、绑定 ViewModel |
|
||||
| `Views/*` | SwiftUI 页面与卡片组件 |
|
||||
|
||||
## 依赖
|
||||
|
||||
- `@Environment(\.cooperationOrderAPI)` 注入网络层
|
||||
- `@EnvironmentObject PermissionContext` 权限校验
|
||||
- `@EnvironmentObject ToastCenter` 用户提示
|
||||
- Kingfisher 加载线索图片
|
||||
@ -0,0 +1,517 @@
|
||||
//
|
||||
// CooperationOrderModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// 非空时返回自身,否则返回 fallback。
|
||||
func cooperationNonEmptyOrDefault(_ fallback: String) -> String {
|
||||
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? fallback : trimmed
|
||||
}
|
||||
}
|
||||
|
||||
/// 带客单状态更新请求体。
|
||||
struct ReferralOrderStatusUpdateRequest: Encodable {
|
||||
let status: Int
|
||||
}
|
||||
|
||||
/// 合作获客员列表响应。
|
||||
struct CooperativeSalerListPayload: Decodable {
|
||||
let total: Int
|
||||
let list: [CooperativeSaler]
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case total
|
||||
case list
|
||||
case page
|
||||
case pageSize = "page_size"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
total = try container.decodeLossyInt(forKey: .total) ?? 0
|
||||
list = try container.decodeIfPresent([CooperativeSaler].self, forKey: .list) ?? []
|
||||
page = try container.decodeLossyInt(forKey: .page) ?? 0
|
||||
pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 合作获客员实体。
|
||||
struct CooperativeSaler: Decodable, Identifiable {
|
||||
let saleUserId: Int
|
||||
let bindingId: Int
|
||||
let name: String
|
||||
let salerName: String
|
||||
let phone: String
|
||||
let phoneMasked: String
|
||||
let salerPhone: String
|
||||
let shareCode: String
|
||||
let storeId: Int
|
||||
let bindTime: String
|
||||
let boundAt: String
|
||||
|
||||
var id: Int { saleUserId }
|
||||
|
||||
var displayId: Int { saleUserId }
|
||||
|
||||
var displayName: String {
|
||||
name.cooperationNonEmptyOrDefault(salerName)
|
||||
}
|
||||
|
||||
var displayPhone: String {
|
||||
salerPhone.cooperationNonEmptyOrDefault(phone)
|
||||
}
|
||||
|
||||
var displayBindTime: String {
|
||||
bindTime.cooperationNonEmptyOrDefault(boundAt)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case bindingId = "binding_id"
|
||||
case name
|
||||
case salerName = "saler_name"
|
||||
case phone
|
||||
case phoneMasked = "phone_masked"
|
||||
case salerPhone = "saler_phone"
|
||||
case shareCode = "share_code"
|
||||
case storeId = "store_id"
|
||||
case bindTime = "bind_time"
|
||||
case boundAt = "bound_at"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0
|
||||
name = try container.decodeLossyString(forKey: .name)
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
phone = try container.decodeLossyString(forKey: .phone)
|
||||
phoneMasked = try container.decodeLossyString(forKey: .phoneMasked)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
shareCode = try container.decodeLossyString(forKey: .shareCode)
|
||||
storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0
|
||||
bindTime = try container.decodeLossyString(forKey: .bindTime)
|
||||
boundAt = try container.decodeLossyString(forKey: .boundAt)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获客员邀请信息,用于绑定确认页展示。
|
||||
struct SaleUserInviteInfo: Decodable {
|
||||
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.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
isBound = try container.decodeLossyBool(forKey: .isBound) ?? false
|
||||
photographerName = try container.decodeLossyString(forKey: .photographerName)
|
||||
photographerPhone = try container.decodeLossyString(forKey: .photographerPhone)
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
shareCode = try container.decodeLossyString(forKey: .shareCode)
|
||||
subtitle = try container.decodeLossyString(forKey: .subtitle)
|
||||
title = try container.decodeLossyString(forKey: .title)
|
||||
}
|
||||
|
||||
init(
|
||||
saleUserId: Int,
|
||||
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 ReferralLead: Decodable, Identifiable {
|
||||
let id: Int
|
||||
let leadId: Int
|
||||
let userPhone: String
|
||||
let userMobileMasked: String
|
||||
let phone: String
|
||||
let remark: String
|
||||
let createdAt: String
|
||||
let imageUrls: [String]
|
||||
let images: [String]
|
||||
|
||||
var displayId: String {
|
||||
let value = leadId > 0 ? leadId : id
|
||||
return "\(value)"
|
||||
}
|
||||
|
||||
var displayPhone: String {
|
||||
userMobileMasked.cooperationNonEmptyOrDefault(userPhone.cooperationNonEmptyOrDefault(phone))
|
||||
}
|
||||
|
||||
var displayImages: [String] {
|
||||
imageUrls.isEmpty ? images : imageUrls
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case leadId = "lead_id"
|
||||
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.decodeLossyInt(forKey: .id) ?? 0
|
||||
leadId = try container.decodeLossyInt(forKey: .leadId) ?? 0
|
||||
userPhone = try container.decodeLossyString(forKey: .userPhone)
|
||||
userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked)
|
||||
phone = try container.decodeLossyString(forKey: .phone)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
|
||||
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 获客订单实体。
|
||||
struct AcquisitionOrder: Decodable, Identifiable {
|
||||
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 orderStatusName: String
|
||||
let status: String
|
||||
let saleUserName: String
|
||||
let partnerName: String
|
||||
let partnerRole: String
|
||||
let remark: String
|
||||
let imageUrls: [String]
|
||||
|
||||
var displayAmount: String {
|
||||
orderAmount.cooperationNonEmptyOrDefault(amount.cooperationNonEmptyOrDefault(actualPayAmount))
|
||||
}
|
||||
|
||||
var displayCustomerPhone: String {
|
||||
userMobileMasked.cooperationNonEmptyOrDefault(customerPhone.cooperationNonEmptyOrDefault(phone))
|
||||
}
|
||||
|
||||
var displayStatus: String {
|
||||
orderStatusName.cooperationNonEmptyOrDefault(status)
|
||||
}
|
||||
|
||||
var displayPartner: String {
|
||||
if !saleUserName.isEmpty { return saleUserName }
|
||||
if !partnerName.isEmpty { return partnerName }
|
||||
return ""
|
||||
}
|
||||
|
||||
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 orderStatusName = "order_status_name"
|
||||
case status
|
||||
case saleUserName = "sale_user_name"
|
||||
case partnerName = "partner_name"
|
||||
case partnerRole = "partner_role"
|
||||
case remark
|
||||
case imageUrls = "image_urls"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||||
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
|
||||
orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel)
|
||||
orderAmount = try container.decodeLossyString(forKey: .orderAmount)
|
||||
amount = try container.decodeLossyString(forKey: .amount)
|
||||
actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
customerPhone = try container.decodeLossyString(forKey: .customerPhone)
|
||||
userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked)
|
||||
phone = try container.decodeLossyString(forKey: .phone)
|
||||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||||
status = try container.decodeLossyString(forKey: .status)
|
||||
saleUserName = try container.decodeLossyString(forKey: .saleUserName)
|
||||
partnerName = try container.decodeLossyString(forKey: .partnerName)
|
||||
partnerRole = try container.decodeLossyString(forKey: .partnerRole)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 带客单列表中的获客员摘要。
|
||||
struct ReferralOrderSaler: Decodable {
|
||||
let saleUserId: Int
|
||||
let salerName: String
|
||||
let salerPhone: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case salerName = "saler_name"
|
||||
case salerPhone = "saler_phone"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可绑定带客单列表响应。
|
||||
struct ReferralOrderListPayload: Decodable {
|
||||
let total: Int
|
||||
let list: [ReferralOrder]
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
let saler: ReferralOrderSaler?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case total
|
||||
case list
|
||||
case page
|
||||
case pageSize = "page_size"
|
||||
case saler
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
total = try container.decodeLossyInt(forKey: .total) ?? 0
|
||||
list = try container.decodeIfPresent([ReferralOrder].self, forKey: .list) ?? []
|
||||
page = try container.decodeLossyInt(forKey: .page) ?? 0
|
||||
pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0
|
||||
saler = try container.decodeIfPresent(ReferralOrderSaler.self, forKey: .saler)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可绑定带客单实体。
|
||||
struct ReferralOrder: Decodable, Identifiable {
|
||||
let id: Int
|
||||
let referralOrderNo: String
|
||||
let orderNo: String
|
||||
let userMobile: String
|
||||
let remark: String
|
||||
let createdAt: String
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let images: [String]
|
||||
|
||||
var displayReferralOrderNo: String {
|
||||
referralOrderNo.cooperationNonEmptyOrDefault(orderNo)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case referralOrderNo = "referral_order_no"
|
||||
case orderNo = "order_no"
|
||||
case userMobile = "user_mobile"
|
||||
case remark
|
||||
case createdAt = "created_at"
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case images
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
referralOrderNo = try container.decodeLossyString(forKey: .referralOrderNo)
|
||||
orderNo = try container.decodeLossyString(forKey: .orderNo)
|
||||
userMobile = try container.decodeLossyString(forKey: .userMobile)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
statusLabel = try container.decodeLossyString(forKey: .statusLabel)
|
||||
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// listv2 已绑定订单来源 referral_order。
|
||||
struct BoundReferralOrder: Decodable, Equatable, Hashable {
|
||||
let id: Int
|
||||
let orderNo: String
|
||||
let saleUserId: Int
|
||||
let salerName: String
|
||||
let salerPhone: String
|
||||
let displayText: String
|
||||
let userMobile: String
|
||||
let remark: String
|
||||
let bindOrderNumber: String
|
||||
let sourceType: Int
|
||||
let sourceTypeLabel: String
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let createdAt: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case orderNo = "order_no"
|
||||
case saleUserId = "sale_user_id"
|
||||
case salerName = "saler_name"
|
||||
case salerPhone = "saler_phone"
|
||||
case displayText = "display_text"
|
||||
case userMobile = "user_mobile"
|
||||
case remark
|
||||
case bindOrderNumber = "bind_order_number"
|
||||
case sourceType = "source_type"
|
||||
case sourceTypeLabel = "source_type_label"
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeLossyInt(forKey: .id) ?? 0
|
||||
orderNo = try container.decodeLossyString(forKey: .orderNo)
|
||||
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
displayText = try container.decodeLossyString(forKey: .displayText)
|
||||
userMobile = try container.decodeLossyString(forKey: .userMobile)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
bindOrderNumber = try container.decodeLossyString(forKey: .bindOrderNumber)
|
||||
sourceType = try container.decodeLossyInt(forKey: .sourceType) ?? 0
|
||||
sourceTypeLabel = try container.decodeLossyString(forKey: .sourceTypeLabel)
|
||||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||||
statusLabel = try container.decodeLossyString(forKey: .statusLabel)
|
||||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫码绑定获客员 Sheet 标识。
|
||||
struct BindAcquirerSheetItem: Identifiable {
|
||||
let saleUserId: Int
|
||||
var id: Int { saleUserId }
|
||||
}
|
||||
|
||||
/// 合作订单模块内部导航路由。
|
||||
enum CooperationOrderRoute: Hashable {
|
||||
case acquirer
|
||||
case bind(saleUserId: Int)
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将任意常见 JSON 值宽松解码成字符串。
|
||||
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 ""
|
||||
}
|
||||
|
||||
/// 将 Int、Double、Bool 或数字字符串宽松解码成整数。
|
||||
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let intValue = Int(text) {
|
||||
return intValue
|
||||
}
|
||||
if let doubleValue = Double(text) {
|
||||
return Int(doubleValue)
|
||||
}
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value ? 1 : 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 Bool、Int 或字符串宽松解码成布尔值。
|
||||
func decodeLossyBool(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) {
|
||||
switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
||||
case "1", "true", "yes":
|
||||
return true
|
||||
case "0", "false", "no":
|
||||
return false
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
//
|
||||
// CooperationOrderFeature.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 合作订单权限特性,负责 URI 校验与访问判断。
|
||||
enum CooperationOrderFeature {
|
||||
static let uri = "cooperation_order"
|
||||
|
||||
/// 判断当前角色是否拥有合作订单权限。
|
||||
static func canAccess(permissionContext: PermissionContext) -> Bool {
|
||||
permissionContext.canAccess(uri)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
//
|
||||
// BindAcquirerViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// 绑定获客员 ViewModel,负责邀请信息加载与接受绑定。
|
||||
@MainActor
|
||||
final class BindAcquirerViewModel: ObservableObject {
|
||||
@Published var inviteInfo: SaleUserInviteInfo?
|
||||
@Published var isLoading = false
|
||||
@Published var isAlreadyBound = false
|
||||
@Published var errorMessage: String?
|
||||
|
||||
private var saleUserId = 0
|
||||
|
||||
/// 加载获客员邀请信息。
|
||||
func load(api: CooperationOrderServing, saleUserId: Int) async {
|
||||
guard saleUserId > 0 else {
|
||||
errorMessage = "获客员信息无效"
|
||||
return
|
||||
}
|
||||
if self.saleUserId == saleUserId && (inviteInfo != nil || isAlreadyBound) { return }
|
||||
self.saleUserId = saleUserId
|
||||
isAlreadyBound = false
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
do {
|
||||
var info = try await api.saleUserInviteInfo(saleUserId: saleUserId)
|
||||
if info.saleUserId <= 0 {
|
||||
info = SaleUserInviteInfo(
|
||||
saleUserId: saleUserId,
|
||||
isBound: info.isBound,
|
||||
photographerName: info.photographerName,
|
||||
photographerPhone: info.photographerPhone,
|
||||
salerName: info.salerName,
|
||||
salerPhone: info.salerPhone,
|
||||
shareCode: info.shareCode,
|
||||
subtitle: info.subtitle,
|
||||
title: info.title
|
||||
)
|
||||
}
|
||||
if info.isBound {
|
||||
isAlreadyBound = true
|
||||
}
|
||||
inviteInfo = info
|
||||
} catch {
|
||||
handleBindError(error.localizedDescription.isEmpty ? "获取邀请信息失败" : error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 提交接受绑定。
|
||||
func acceptBind(api: CooperationOrderServing) async throws {
|
||||
guard saleUserId > 0, !isAlreadyBound else { return }
|
||||
try await api.saleUserAcceptBind(saleUserId: saleUserId)
|
||||
}
|
||||
|
||||
/// 处理绑定错误,识别已绑定状态。
|
||||
func handleBindError(_ message: String) {
|
||||
errorMessage = message
|
||||
if message.contains("已绑定") {
|
||||
isAlreadyBound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
//
|
||||
// CooperationAcquirerViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// 合作获客员列表 ViewModel。
|
||||
@MainActor
|
||||
final class CooperationAcquirerViewModel: ObservableObject {
|
||||
@Published var acquirers: [CooperativeSaler] = []
|
||||
@Published private(set) var hasLoadedOnce = false
|
||||
|
||||
/// 重新加载当前门店下的合作获客员。
|
||||
func reload(api: CooperationOrderServing, storeId: Int?) async {
|
||||
do {
|
||||
let storeParam = storeId.flatMap { $0 > 0 ? $0 : nil }
|
||||
let data = try await api.cooperativeSalers(storeId: storeParam, page: 1, pageSize: 20)
|
||||
acquirers = data.list
|
||||
} catch {
|
||||
if !hasLoadedOnce {
|
||||
acquirers = []
|
||||
}
|
||||
}
|
||||
hasLoadedOnce = true
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
//
|
||||
// CooperationOrderListViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// 合作订单列表 ViewModel,管理获客线索与获客订单双 Tab 分页。
|
||||
@MainActor
|
||||
final class CooperationOrderListViewModel: ObservableObject {
|
||||
@Published var selectedTab = 0
|
||||
@Published var searchText = ""
|
||||
@Published var leads: [ReferralLead] = []
|
||||
@Published var orders: [AcquisitionOrder] = []
|
||||
@Published var isLoadingMore = false
|
||||
@Published var canLoadMore = false
|
||||
@Published private(set) var hasLoadedLeadsOnce = false
|
||||
@Published private(set) var hasLoadedOrdersOnce = false
|
||||
|
||||
private var leadsPage = 1
|
||||
private var ordersPage = 1
|
||||
private var leadsTotal = 0
|
||||
private var ordersTotal = 0
|
||||
private var isReloading = false
|
||||
private let pageSize = 10
|
||||
|
||||
/// 切换 Tab 并触发对应列表刷新。
|
||||
func onTabSelected(_ index: Int) {
|
||||
guard selectedTab != index else { return }
|
||||
selectedTab = index
|
||||
}
|
||||
|
||||
/// 刷新当前 Tab 列表。
|
||||
func refreshCurrentTab(api: CooperationOrderServing) async {
|
||||
if selectedTab == 0 {
|
||||
await refreshLeads(api: api)
|
||||
} else {
|
||||
await refreshOrders(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新获客线索列表。
|
||||
func refreshLeads(api: CooperationOrderServing) async {
|
||||
guard !isReloading else { return }
|
||||
isReloading = true
|
||||
defer { isReloading = false }
|
||||
leadsPage = 1
|
||||
isLoadingMore = false
|
||||
await loadLeads(api: api)
|
||||
}
|
||||
|
||||
/// 刷新获客订单列表。
|
||||
func refreshOrders(api: CooperationOrderServing) async {
|
||||
guard !isReloading else { return }
|
||||
isReloading = true
|
||||
defer { isReloading = false }
|
||||
ordersPage = 1
|
||||
isLoadingMore = false
|
||||
await loadOrders(api: api)
|
||||
}
|
||||
|
||||
/// 加载更多当前 Tab 数据。
|
||||
func loadMore(api: CooperationOrderServing) async {
|
||||
guard !isReloading, !isLoadingMore, canLoadMore else { return }
|
||||
isLoadingMore = true
|
||||
if selectedTab == 0 {
|
||||
leadsPage += 1
|
||||
await loadLeads(api: api)
|
||||
} else {
|
||||
ordersPage += 1
|
||||
await loadOrders(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
/// 按关键词搜索获客订单。
|
||||
func searchOrders(api: CooperationOrderServing) async {
|
||||
guard selectedTab == 1 else { return }
|
||||
await refreshOrders(api: api)
|
||||
}
|
||||
|
||||
private func loadLeads(api: CooperationOrderServing) async {
|
||||
do {
|
||||
let data = try await api.saleUserReferralLeads(page: leadsPage, pageSize: pageSize)
|
||||
leadsTotal = data.total
|
||||
if leadsPage == 1 {
|
||||
leads = data.list
|
||||
} else {
|
||||
leads.append(contentsOf: data.list)
|
||||
}
|
||||
canLoadMore = leads.count < leadsTotal
|
||||
} catch {
|
||||
if leadsPage == 1, !hasLoadedLeadsOnce {
|
||||
leads = []
|
||||
}
|
||||
}
|
||||
if leadsPage == 1 {
|
||||
hasLoadedLeadsOnce = true
|
||||
}
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
||||
private func loadOrders(api: CooperationOrderServing) async {
|
||||
do {
|
||||
let keyword = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let data = try await api.saleUserAcquisitionOrders(
|
||||
keyword: keyword.isEmpty ? nil : keyword,
|
||||
page: ordersPage,
|
||||
pageSize: pageSize
|
||||
)
|
||||
ordersTotal = data.total
|
||||
if ordersPage == 1 {
|
||||
orders = data.list
|
||||
} else {
|
||||
orders.append(contentsOf: data.list)
|
||||
}
|
||||
canLoadMore = orders.count < ordersTotal
|
||||
} catch {
|
||||
if ordersPage == 1, !hasLoadedOrdersOnce {
|
||||
orders = []
|
||||
}
|
||||
}
|
||||
if ordersPage == 1 {
|
||||
hasLoadedOrdersOnce = true
|
||||
}
|
||||
isLoadingMore = false
|
||||
}
|
||||
}
|
||||
141
suixinkan/Features/CooperationOrder/Views/BindAcquirerView.swift
Normal file
141
suixinkan/Features/CooperationOrder/Views/BindAcquirerView.swift
Normal file
@ -0,0 +1,141 @@
|
||||
//
|
||||
// BindAcquirerView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 绑定获客员确认页,展示邀请信息并提交接受绑定。
|
||||
struct BindAcquirerView: View {
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var viewModel = BindAcquirerViewModel()
|
||||
let saleUserId: Int
|
||||
let onSuccess: () -> Void
|
||||
@State private var binding = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
if viewModel.isLoading && viewModel.inviteInfo == nil && !viewModel.isAlreadyBound {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
} else if let info = viewModel.inviteInfo {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(info.title.cooperationNonEmptyOrDefault("绑定获客员"))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
if !info.subtitle.isEmpty {
|
||||
Text(info.subtitle)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.white.opacity(0.9))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.background(AppDesign.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
|
||||
infoCard(title: "获客员", value: info.salerName)
|
||||
infoCard(title: "手机号", value: info.salerPhone)
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
Task { await acceptBind() }
|
||||
} label: {
|
||||
Text(binding ? "提交中..." : (viewModel.isAlreadyBound ? "已绑定获客员" : "接受邀请"))
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 48)
|
||||
.background(
|
||||
viewModel.isAlreadyBound ? AppDesign.textSecondary : AppDesign.primary,
|
||||
in: RoundedRectangle(cornerRadius: 8)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(binding || viewModel.isAlreadyBound)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 12)
|
||||
} else if viewModel.isAlreadyBound {
|
||||
Spacer()
|
||||
Text("已绑定获客员")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
Spacer()
|
||||
|
||||
Button {} label: {
|
||||
Text("已绑定获客员")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 48)
|
||||
.background(AppDesign.textSecondary, in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(true)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 12)
|
||||
} else if let error = viewModel.errorMessage {
|
||||
Spacer()
|
||||
Text(error)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.navigationTitle(viewModel.inviteInfo?.title.cooperationNonEmptyOrDefault("接受邀请") ?? "接受邀请")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(Color(hex: 0xF5F7FA).ignoresSafeArea())
|
||||
.task {
|
||||
await viewModel.load(api: cooperationOrderAPI, saleUserId: saleUserId)
|
||||
if viewModel.errorMessage != nil && viewModel.inviteInfo == nil && !viewModel.isAlreadyBound {
|
||||
if let message = viewModel.errorMessage {
|
||||
toastCenter.show(message)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func infoCard(title: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(AppDesign.textSecondary)
|
||||
Text(value.isEmpty ? "--" : value)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(AppDesign.textPrimary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(14)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
private func acceptBind() async {
|
||||
binding = true
|
||||
defer { binding = false }
|
||||
do {
|
||||
try await viewModel.acceptBind(api: cooperationOrderAPI)
|
||||
toastCenter.show("绑定成功")
|
||||
onSuccess()
|
||||
dismiss()
|
||||
} catch {
|
||||
viewModel.handleBindError(error.localizedDescription.isEmpty ? "绑定失败" : error.localizedDescription)
|
||||
if let message = viewModel.errorMessage {
|
||||
toastCenter.show(message)
|
||||
}
|
||||
if !viewModel.isAlreadyBound {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
//
|
||||
// CooperationAcquirerView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 合作获客员列表页,支持查看已绑定获客员与扫码绑定。
|
||||
struct CooperationAcquirerView: View {
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@Environment(\.openURL) private var openURL
|
||||
@StateObject private var viewModel = CooperationAcquirerViewModel()
|
||||
@State private var showScanner = false
|
||||
let onBind: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
if viewModel.hasLoadedOnce && viewModel.acquirers.isEmpty {
|
||||
Text("暂无合作获客员")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 48)
|
||||
} else {
|
||||
ForEach(viewModel.acquirers, id: \.displayId) { acquirer in
|
||||
CooperativeSalerCard(acquirer: acquirer) { phone in
|
||||
dialPhone(phone)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.navigationTitle("合作获客员")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("扫码绑定") {
|
||||
showScanner = true
|
||||
}
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
}
|
||||
}
|
||||
.background(CooperationOrderColors.pageBg.ignoresSafeArea())
|
||||
.refreshable {
|
||||
await reload(showLoading: false)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showScanner) {
|
||||
OrderScannerPage(
|
||||
onClose: { showScanner = false },
|
||||
onSuccess: { raw in
|
||||
showScanner = false
|
||||
if let saleUserId = SaleUserQrParser.parseSaleUserId(raw) {
|
||||
onBind(saleUserId)
|
||||
} else {
|
||||
toastCenter.show("请扫描正确的获客员二维码")
|
||||
}
|
||||
},
|
||||
onFailure: { error in
|
||||
showScanner = false
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
)
|
||||
}
|
||||
.task {
|
||||
await reload(showLoading: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func reload(showLoading: Bool) async {
|
||||
await globalLoading.withOptionalLoading(showLoading) {
|
||||
await viewModel.reload(api: cooperationOrderAPI, storeId: accountContext.currentStore?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func dialPhone(_ phone: String) {
|
||||
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, let url = URL(string: "tel:\(trimmed)") else { return }
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
//
|
||||
// CooperationOrderAccessView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 合作订单权限入口,无权限时提示并返回。
|
||||
struct CooperationOrderAccessView: View {
|
||||
@EnvironmentObject private var permissionContext: PermissionContext
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if CooperationOrderFeature.canAccess(permissionContext: permissionContext) {
|
||||
CooperationOrderListView()
|
||||
} else {
|
||||
Color.clear
|
||||
.onAppear {
|
||||
toastCenter.show("暂无合作订单权限")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,379 @@
|
||||
//
|
||||
// CooperationOrderComponents.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
enum CooperationOrderColors {
|
||||
static let primary = Color(hex: 0x0073FF)
|
||||
static let text333 = Color(hex: 0x333333)
|
||||
static let text666 = Color(hex: 0x666666)
|
||||
static let text999 = Color(hex: 0x999999)
|
||||
static let searchPlaceholder = Color(hex: 0xB6BECA)
|
||||
static let searchBg = Color(hex: 0xF4F4F4)
|
||||
static let imageEmptyBg = Color(hex: 0xF7F8FA)
|
||||
static let imagePlaceholderBg = Color(hex: 0xF4F4F4)
|
||||
static let pageBg = Color(hex: 0xF5F7FA)
|
||||
}
|
||||
|
||||
struct CooperationCard<Content: View>: View {
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
content()
|
||||
.padding(14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
||||
.shadow(color: Color.black.opacity(0.06), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperationListInfoRow: View {
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text(label)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(value)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.padding(.leading, 14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperationTypeTag: View {
|
||||
let text: String
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(CooperationOrderColors.primary.opacity(0.09), in: RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperationOrderTabs: View {
|
||||
let selectedTab: Int
|
||||
let onTabSelected: (Int) -> Void
|
||||
|
||||
private let tabs = ["获客员线索", "获客订单"]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Array(tabs.enumerated()), id: \.offset) { index, title in
|
||||
Button {
|
||||
onTabSelected(index)
|
||||
} label: {
|
||||
Text(title)
|
||||
.font(.system(size: 15, weight: selectedTab == index ? .bold : .regular))
|
||||
.foregroundStyle(selectedTab == index ? CooperationOrderColors.primary : CooperationOrderColors.text666)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 48)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
GeometryReader { geo in
|
||||
let tabWidth = geo.size.width / CGFloat(tabs.count)
|
||||
RoundedRectangle(cornerRadius: 1.5)
|
||||
.fill(CooperationOrderColors.primary)
|
||||
.frame(width: max(tabWidth - 68, 0), height: 3)
|
||||
.offset(x: tabWidth * CGFloat(selectedTab) + 34, y: 0)
|
||||
}
|
||||
.frame(height: 3)
|
||||
}
|
||||
.background(.white)
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperationOrderSearchBar: View {
|
||||
@Binding var searchText: String
|
||||
let onSearch: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
TextField(
|
||||
"",
|
||||
text: $searchText,
|
||||
prompt: Text("名称搜索").foregroundColor(CooperationOrderColors.searchPlaceholder)
|
||||
)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.submitLabel(.search)
|
||||
.onSubmit(onSearch)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 38)
|
||||
.background(CooperationOrderColors.searchBg, in: RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
.background(.white)
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperationImagePreviewItem: Identifiable {
|
||||
let urls: [String]
|
||||
let index: Int
|
||||
var id: String { "\(index)-\(urls.joined())" }
|
||||
}
|
||||
|
||||
struct ReferralLeadCard: View {
|
||||
let lead: ReferralLead
|
||||
|
||||
@State private var previewIndex: Int?
|
||||
@State private var previewImages: [String] = []
|
||||
|
||||
var body: some View {
|
||||
CooperationCard {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("用户手机号")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(lead.displayPhone.isEmpty ? "—" : lead.displayPhone)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.padding(.leading, 14)
|
||||
Spacer(minLength: 8)
|
||||
Text(lead.createdAt)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
}
|
||||
|
||||
Text("备注")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(lead.remark.isEmpty ? "暂无备注" : lead.remark)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.lineSpacing(7)
|
||||
|
||||
Text("图片")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
ReferralLeadImageGrid(images: lead.displayImages) { images, index in
|
||||
previewImages = images
|
||||
previewIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: previewBinding) { item in
|
||||
SimpleImagePreviewSheet(
|
||||
imageUrls: item.urls,
|
||||
startIndex: item.index,
|
||||
onDismiss: { previewIndex = nil }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var previewBinding: Binding<CooperationImagePreviewItem?> {
|
||||
Binding(
|
||||
get: {
|
||||
guard let previewIndex else { return nil }
|
||||
return CooperationImagePreviewItem(urls: previewImages, index: previewIndex)
|
||||
},
|
||||
set: { newValue in
|
||||
if newValue == nil { previewIndex = nil }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReferralLeadImageGrid: View {
|
||||
let images: [String]
|
||||
var onImageTap: ([String], Int) -> Void = { _, _ in }
|
||||
|
||||
var body: some View {
|
||||
if images.isEmpty {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(CooperationOrderColors.imageEmptyBg)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 72)
|
||||
Text("暂无图片")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<3, id: \.self) { index in
|
||||
if index < images.count, let url = URL(string: images[index]) {
|
||||
KFImage(url)
|
||||
.placeholder { CooperationOrderColors.imagePlaceholderBg }
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
onImageTap(images, index)
|
||||
}
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(maxWidth: .infinity)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AcquisitionOrderCard: View {
|
||||
let order: AcquisitionOrder
|
||||
|
||||
var body: some View {
|
||||
CooperationCard {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(order.projectName.isEmpty ? "—" : order.projectName)
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
.foregroundStyle(CooperationOrderColors.text333)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if !order.orderTypeLabel.isEmpty {
|
||||
CooperationTypeTag(text: order.orderTypeLabel)
|
||||
}
|
||||
Text("订单号 \(order.orderNumber)")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
|
||||
CooperationListInfoRow(
|
||||
label: "合作人员",
|
||||
value: order.displayPartner.isEmpty ? "暂未绑定" : order.displayPartner
|
||||
)
|
||||
CooperationListInfoRow(
|
||||
label: "下单时间",
|
||||
value: order.createdAt.isEmpty ? "—" : order.createdAt
|
||||
)
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("客户 \(order.displayCustomerPhone.isEmpty ? "—" : order.displayCustomerPhone)")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Text("¥\(order.displayAmount.isEmpty ? "—" : order.displayAmount)")
|
||||
.font(.system(size: 17, weight: .bold))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CooperativeSalerCard: View {
|
||||
let acquirer: CooperativeSaler
|
||||
let onDial: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 20))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.padding(10)
|
||||
.background(CooperationOrderColors.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
AcquirerInfoRow(
|
||||
label: "获客员名称",
|
||||
value: acquirer.displayName.isEmpty ? "—" : acquirer.displayName,
|
||||
emphasize: true
|
||||
)
|
||||
AcquirerPhoneInfoRow(phone: acquirer.displayPhone, onDial: onDial)
|
||||
AcquirerInfoRow(
|
||||
label: "绑定时间",
|
||||
value: acquirer.displayBindTime.isEmpty ? "—" : acquirer.displayBindTime
|
||||
)
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: 10))
|
||||
.shadow(color: Color.black.opacity(0.06), radius: 1, x: 0, y: 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct AcquirerInfoRow: View {
|
||||
let label: String
|
||||
let value: String
|
||||
var emphasize: Bool = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text(label)
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(value)
|
||||
.font(.system(size: emphasize ? 15 : 13, weight: emphasize ? .bold : .regular))
|
||||
.foregroundStyle(emphasize ? CooperationOrderColors.text333 : CooperationOrderColors.text666)
|
||||
.padding(.leading, 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct AcquirerPhoneInfoRow: View {
|
||||
let phone: String
|
||||
let onDial: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("手机号")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
Text(maskAcquirerPhone(phone))
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.text666)
|
||||
.padding(.leading, 12)
|
||||
Spacer(minLength: 8)
|
||||
if !phone.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
Button {
|
||||
onDial(phone)
|
||||
} label: {
|
||||
Image(systemName: "phone.fill")
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(Color(hex: 0xEFF6FF), in: Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
func maskAcquirerPhone(_ phone: String) -> String {
|
||||
let digits = phone.filter(\.isNumber)
|
||||
guard digits.count >= 7 else { return phone.isEmpty ? "—" : phone }
|
||||
return "\(digits.prefix(3))****\(digits.suffix(4))"
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
//
|
||||
// CooperationOrderListView.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 合作订单主页,展示获客线索与获客订单双 Tab 列表。
|
||||
struct CooperationOrderListView: View {
|
||||
@Environment(\.cooperationOrderAPI) private var cooperationOrderAPI
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@StateObject private var viewModel = CooperationOrderListViewModel()
|
||||
@State private var route: CooperationOrderRoute?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
CooperationOrderTabs(
|
||||
selectedTab: viewModel.selectedTab,
|
||||
onTabSelected: { viewModel.onTabSelected($0) }
|
||||
)
|
||||
|
||||
if viewModel.selectedTab == 1 {
|
||||
CooperationOrderSearchBar(searchText: $viewModel.searchText) {
|
||||
Task { await searchOrders(showLoading: true) }
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 12) {
|
||||
if viewModel.selectedTab == 0 {
|
||||
if viewModel.hasLoadedLeadsOnce && viewModel.leads.isEmpty {
|
||||
emptyState("暂无获客员线索")
|
||||
} else {
|
||||
ForEach(Array(viewModel.leads.enumerated()), id: \.element.id) { index, lead in
|
||||
ReferralLeadCard(lead: lead)
|
||||
.onAppear {
|
||||
guard index >= viewModel.leads.count - 3 else { return }
|
||||
Task { await viewModel.loadMore(api: cooperationOrderAPI) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if viewModel.hasLoadedOrdersOnce && viewModel.orders.isEmpty {
|
||||
emptyState("暂无相关合作订单")
|
||||
} else {
|
||||
ForEach(Array(viewModel.orders.enumerated()), id: \.element.id) { index, order in
|
||||
AcquisitionOrderCard(order: order)
|
||||
.onAppear {
|
||||
guard index >= viewModel.orders.count - 3 else { return }
|
||||
Task { await viewModel.loadMore(api: cooperationOrderAPI) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
ProgressView()
|
||||
.tint(CooperationOrderColors.primary)
|
||||
.frame(width: 24, height: 24)
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.refreshable {
|
||||
await reloadCurrentTab(showLoading: false)
|
||||
}
|
||||
}
|
||||
.navigationTitle("合作订单")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("合作获客员") {
|
||||
route = .acquirer
|
||||
}
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
}
|
||||
}
|
||||
.background(CooperationOrderColors.pageBg.ignoresSafeArea())
|
||||
.appNavigationDestination(item: $route) { destination in
|
||||
switch destination {
|
||||
case .acquirer:
|
||||
CooperationAcquirerView(onBind: { saleUserId in
|
||||
route = .bind(saleUserId: saleUserId)
|
||||
})
|
||||
case let .bind(saleUserId):
|
||||
BindAcquirerView(saleUserId: saleUserId, onSuccess: {
|
||||
route = .acquirer
|
||||
})
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await reloadCurrentTab(showLoading: true)
|
||||
}
|
||||
.onChange(of: viewModel.selectedTab) { _ in
|
||||
Task { await reloadCurrentTab(showLoading: true) }
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadCurrentTab(showLoading: Bool) async {
|
||||
await globalLoading.withOptionalLoading(showLoading) {
|
||||
await viewModel.refreshCurrentTab(api: cooperationOrderAPI)
|
||||
}
|
||||
}
|
||||
|
||||
private func searchOrders(showLoading: Bool) async {
|
||||
await globalLoading.withOptionalLoading(showLoading) {
|
||||
await viewModel.searchOrders(api: cooperationOrderAPI)
|
||||
}
|
||||
}
|
||||
|
||||
private func emptyState(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(CooperationOrderColors.text999)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 48)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user