拆分宣传页视图并扩充 Mock 数据,合作订单支持获客员备注名并修复线索图片网格尺寸。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -33,6 +33,9 @@ protocol CooperationOrderServing {
|
||||
|
||||
/// 更新带客单状态(如退款时 status=3)。
|
||||
func saleUserUpdateReferralOrderStatus(id: Int, status: Int) async throws
|
||||
|
||||
/// 更新合作获客员备注名。
|
||||
func saleUserUpdateRemark(saleUserId: Int, photogRemarkName: String) async throws
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@ -156,4 +159,18 @@ final class CooperationOrderAPI: CooperationOrderServing {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新合作获客员备注名。
|
||||
func saleUserUpdateRemark(saleUserId: Int, photogRemarkName: String) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/yf-handset-app/photog/sale-user/update-remark",
|
||||
body: SaleUserUpdateRemarkRequest(
|
||||
saleUserId: saleUserId,
|
||||
photogRemarkName: photogRemarkName
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,13 @@
|
||||
2. 扫码或主 Tab 扫码识别 `{"method":"sale_user_bind","sale_user_id":N}`
|
||||
3. `BindAcquirerView` 加载邀请信息 → 确认 → `POST .../sale-user/accept-bind`
|
||||
|
||||
### 获客员备注名
|
||||
|
||||
1. 合作获客员列表每行右上角「修改备注」→ 弹窗编辑备注名
|
||||
2. 保存 → `POST .../sale-user/update-remark`,Body:`{ "sale_user_id": Int, "photog_remark_name": String }`
|
||||
3. 展示规则:优先 `photog_remark_name`,为空时回退获客员原名(`name` / `saler_name` / `sale_user_name`)
|
||||
4. 绑定确认页(`BindAcquirerView`)仍展示真实获客员名,不使用备注名
|
||||
|
||||
## 与订单模块关系
|
||||
|
||||
订单管理(`orderType` 11 摄影师跟拍 / 14 线下扫码)在订单卡片展示「订单来源」:
|
||||
@ -43,7 +50,7 @@
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `API/CooperationOrderAPI.swift` | 8 个 sale-user / cooperative 接口 |
|
||||
| `API/CooperationOrderAPI.swift` | 9 个 sale-user / cooperative 接口 |
|
||||
| `Models/CooperationOrderModels.swift` | 实体与解码 |
|
||||
| `Services/CooperationOrderFeature.swift` | 权限 URI 常量 |
|
||||
| `ViewModels/*` | 列表、获客员、绑定 ViewModel |
|
||||
|
||||
@ -13,6 +13,11 @@ extension String {
|
||||
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? fallback : trimmed
|
||||
}
|
||||
|
||||
/// 获客员展示名:优先备注名,其次主名称,最后回退名称。
|
||||
func saleUserDisplayName(primaryName: String, fallbackName: String = "") -> String {
|
||||
cooperationNonEmptyOrDefault(primaryName.cooperationNonEmptyOrDefault(fallbackName))
|
||||
}
|
||||
}
|
||||
|
||||
/// 带客单状态更新请求体。
|
||||
@ -20,6 +25,17 @@ struct ReferralOrderStatusUpdateRequest: Encodable {
|
||||
let status: Int
|
||||
}
|
||||
|
||||
/// 合作获客员备注名更新请求体。
|
||||
struct SaleUserUpdateRemarkRequest: Encodable {
|
||||
let saleUserId: Int
|
||||
let photogRemarkName: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
}
|
||||
}
|
||||
|
||||
/// 合作获客员列表响应。
|
||||
struct CooperativeSalerListPayload: Decodable {
|
||||
let total: Int
|
||||
@ -49,6 +65,7 @@ struct CooperativeSaler: Decodable, Identifiable {
|
||||
let bindingId: Int
|
||||
let name: String
|
||||
let salerName: String
|
||||
let photogRemarkName: String
|
||||
let phone: String
|
||||
let phoneMasked: String
|
||||
let salerPhone: String
|
||||
@ -62,7 +79,7 @@ struct CooperativeSaler: Decodable, Identifiable {
|
||||
var displayId: Int { saleUserId }
|
||||
|
||||
var displayName: String {
|
||||
name.cooperationNonEmptyOrDefault(salerName)
|
||||
photogRemarkName.saleUserDisplayName(primaryName: name, fallbackName: salerName)
|
||||
}
|
||||
|
||||
var displayPhone: String {
|
||||
@ -78,6 +95,7 @@ struct CooperativeSaler: Decodable, Identifiable {
|
||||
case bindingId = "binding_id"
|
||||
case name
|
||||
case salerName = "saler_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case phone
|
||||
case phoneMasked = "phone_masked"
|
||||
case salerPhone = "saler_phone"
|
||||
@ -93,6 +111,7 @@ struct CooperativeSaler: Decodable, Identifiable {
|
||||
bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0
|
||||
name = try container.decodeLossyString(forKey: .name)
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
||||
phone = try container.decodeLossyString(forKey: .phone)
|
||||
phoneMasked = try container.decodeLossyString(forKey: .phoneMasked)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
@ -101,6 +120,52 @@ struct CooperativeSaler: Decodable, Identifiable {
|
||||
bindTime = try container.decodeLossyString(forKey: .bindTime)
|
||||
boundAt = try container.decodeLossyString(forKey: .boundAt)
|
||||
}
|
||||
|
||||
/// 返回更新了备注名的副本,用于本地列表即时刷新。
|
||||
func withPhotogRemarkName(_ remark: String) -> CooperativeSaler {
|
||||
CooperativeSaler(
|
||||
saleUserId: saleUserId,
|
||||
bindingId: bindingId,
|
||||
name: name,
|
||||
salerName: salerName,
|
||||
photogRemarkName: remark,
|
||||
phone: phone,
|
||||
phoneMasked: phoneMasked,
|
||||
salerPhone: salerPhone,
|
||||
shareCode: shareCode,
|
||||
storeId: storeId,
|
||||
bindTime: bindTime,
|
||||
boundAt: boundAt
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
saleUserId: Int,
|
||||
bindingId: Int,
|
||||
name: String,
|
||||
salerName: String,
|
||||
photogRemarkName: String,
|
||||
phone: String,
|
||||
phoneMasked: String,
|
||||
salerPhone: String,
|
||||
shareCode: String,
|
||||
storeId: Int,
|
||||
bindTime: String,
|
||||
boundAt: String
|
||||
) {
|
||||
self.saleUserId = saleUserId
|
||||
self.bindingId = bindingId
|
||||
self.name = name
|
||||
self.salerName = salerName
|
||||
self.photogRemarkName = photogRemarkName
|
||||
self.phone = phone
|
||||
self.phoneMasked = phoneMasked
|
||||
self.salerPhone = salerPhone
|
||||
self.shareCode = shareCode
|
||||
self.storeId = storeId
|
||||
self.bindTime = bindTime
|
||||
self.boundAt = boundAt
|
||||
}
|
||||
}
|
||||
|
||||
/// 获客员邀请信息,用于绑定确认页展示。
|
||||
@ -231,6 +296,7 @@ struct AcquisitionOrder: Decodable, Identifiable {
|
||||
let orderStatusName: String
|
||||
let status: String
|
||||
let saleUserName: String
|
||||
let photogRemarkName: String
|
||||
let partnerName: String
|
||||
let partnerRole: String
|
||||
let remark: String
|
||||
@ -249,7 +315,8 @@ struct AcquisitionOrder: Decodable, Identifiable {
|
||||
}
|
||||
|
||||
var displayPartner: String {
|
||||
if !saleUserName.isEmpty { return saleUserName }
|
||||
let salerDisplayName = photogRemarkName.saleUserDisplayName(primaryName: saleUserName)
|
||||
if !salerDisplayName.isEmpty { return salerDisplayName }
|
||||
if !partnerName.isEmpty { return partnerName }
|
||||
return ""
|
||||
}
|
||||
@ -270,6 +337,7 @@ struct AcquisitionOrder: Decodable, Identifiable {
|
||||
case orderStatusName = "order_status_name"
|
||||
case status
|
||||
case saleUserName = "sale_user_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case partnerName = "partner_name"
|
||||
case partnerRole = "partner_role"
|
||||
case remark
|
||||
@ -293,6 +361,7 @@ struct AcquisitionOrder: Decodable, Identifiable {
|
||||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||||
status = try container.decodeLossyString(forKey: .status)
|
||||
saleUserName = try container.decodeLossyString(forKey: .saleUserName)
|
||||
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
||||
partnerName = try container.decodeLossyString(forKey: .partnerName)
|
||||
partnerRole = try container.decodeLossyString(forKey: .partnerRole)
|
||||
remark = try container.decodeLossyString(forKey: .remark)
|
||||
@ -304,11 +373,17 @@ struct AcquisitionOrder: Decodable, Identifiable {
|
||||
struct ReferralOrderSaler: Decodable {
|
||||
let saleUserId: Int
|
||||
let salerName: String
|
||||
let photogRemarkName: String
|
||||
let salerPhone: String
|
||||
|
||||
var displayName: String {
|
||||
photogRemarkName.saleUserDisplayName(primaryName: salerName)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case saleUserId = "sale_user_id"
|
||||
case salerName = "saler_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case salerPhone = "saler_phone"
|
||||
}
|
||||
|
||||
@ -316,6 +391,7 @@ struct ReferralOrderSaler: Decodable {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
}
|
||||
}
|
||||
@ -394,6 +470,7 @@ struct BoundReferralOrder: Decodable, Equatable, Hashable {
|
||||
let orderNo: String
|
||||
let saleUserId: Int
|
||||
let salerName: String
|
||||
let photogRemarkName: String
|
||||
let salerPhone: String
|
||||
let displayText: String
|
||||
let userMobile: String
|
||||
@ -405,11 +482,16 @@ struct BoundReferralOrder: Decodable, Equatable, Hashable {
|
||||
let statusLabel: String
|
||||
let createdAt: String
|
||||
|
||||
var displaySalerName: String {
|
||||
photogRemarkName.saleUserDisplayName(primaryName: salerName)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case orderNo = "order_no"
|
||||
case saleUserId = "sale_user_id"
|
||||
case salerName = "saler_name"
|
||||
case photogRemarkName = "photog_remark_name"
|
||||
case salerPhone = "saler_phone"
|
||||
case displayText = "display_text"
|
||||
case userMobile = "user_mobile"
|
||||
@ -428,6 +510,7 @@ struct BoundReferralOrder: Decodable, Equatable, Hashable {
|
||||
orderNo = try container.decodeLossyString(forKey: .orderNo)
|
||||
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
|
||||
salerName = try container.decodeLossyString(forKey: .salerName)
|
||||
photogRemarkName = try container.decodeLossyString(forKey: .photogRemarkName)
|
||||
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
|
||||
displayText = try container.decodeLossyString(forKey: .displayText)
|
||||
userMobile = try container.decodeLossyString(forKey: .userMobile)
|
||||
|
||||
@ -27,4 +27,19 @@ final class CooperationAcquirerViewModel: ObservableObject {
|
||||
}
|
||||
hasLoadedOnce = true
|
||||
}
|
||||
|
||||
/// 更新指定获客员的备注名,成功后刷新本地列表项。
|
||||
func updateRemark(
|
||||
saleUserId: Int,
|
||||
remark: String,
|
||||
api: CooperationOrderServing
|
||||
) async throws {
|
||||
guard saleUserId > 0 else { return }
|
||||
let trimmedRemark = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try await api.saleUserUpdateRemark(saleUserId: saleUserId, photogRemarkName: trimmedRemark)
|
||||
acquirers = acquirers.map { acquirer in
|
||||
guard acquirer.saleUserId == saleUserId else { return acquirer }
|
||||
return acquirer.withPhotogRemarkName(trimmedRemark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,9 @@ struct CooperationAcquirerView: View {
|
||||
@Environment(\.openURL) private var openURL
|
||||
@StateObject private var viewModel = CooperationAcquirerViewModel()
|
||||
@State private var showScanner = false
|
||||
@State private var showRemarkAlert = false
|
||||
@State private var remarkEditingAcquirer: CooperativeSaler?
|
||||
@State private var remarkDraft = ""
|
||||
let onBind: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
@ -31,6 +34,10 @@ struct CooperationAcquirerView: View {
|
||||
ForEach(viewModel.acquirers, id: \.displayId) { acquirer in
|
||||
CooperativeSalerCard(acquirer: acquirer) { phone in
|
||||
dialPhone(phone)
|
||||
} onEditRemark: {
|
||||
remarkEditingAcquirer = acquirer
|
||||
remarkDraft = acquirer.photogRemarkName
|
||||
showRemarkAlert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,6 +80,44 @@ struct CooperationAcquirerView: View {
|
||||
.task {
|
||||
await reload(showLoading: true)
|
||||
}
|
||||
.alert("修改备注", isPresented: $showRemarkAlert) {
|
||||
TextField("备注名", text: $remarkDraft)
|
||||
Button("取消", role: .cancel) {
|
||||
remarkEditingAcquirer = nil
|
||||
}
|
||||
Button("保存") {
|
||||
// 必须在弹窗关闭前同步捕获,避免 alert dismiss 清空 state 导致接口未调用。
|
||||
guard let acquirer = remarkEditingAcquirer else { return }
|
||||
let saleUserId = acquirer.saleUserId
|
||||
let draft = remarkDraft
|
||||
showRemarkAlert = false
|
||||
remarkEditingAcquirer = nil
|
||||
Task {
|
||||
await saveRemark(saleUserId: saleUserId, draft: draft)
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
if let acquirer = remarkEditingAcquirer {
|
||||
Text("为 \(acquirer.salerName.cooperationNonEmptyOrDefault(acquirer.name)) 设置备注名")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存当前编辑中的获客员备注名。
|
||||
private func saveRemark(saleUserId: Int, draft: String) async {
|
||||
guard saleUserId > 0 else { return }
|
||||
await globalLoading.withOptionalLoading(true) {
|
||||
do {
|
||||
try await viewModel.updateRemark(
|
||||
saleUserId: saleUserId,
|
||||
remark: draft,
|
||||
api: cooperationOrderAPI
|
||||
)
|
||||
toastCenter.show("备注已保存")
|
||||
} catch {
|
||||
toastCenter.show(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reload(showLoading: Bool) async {
|
||||
|
||||
@ -218,26 +218,37 @@ struct ReferralLeadImageGrid: View {
|
||||
} else {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<3, id: \.self) { index in
|
||||
if index < images.count, let url = URL(string: images[index]) {
|
||||
imageCell(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染单个网格位,先固定 1:1 容器再填充图片,避免 Kingfisher 按原图尺寸撑开导致格子大小不一。
|
||||
@ViewBuilder
|
||||
private func imageCell(at index: Int) -> some View {
|
||||
Group {
|
||||
if index < images.count, let url = URL(string: images[index]) {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(CooperationOrderColors.imagePlaceholderBg)
|
||||
.overlay {
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
guard index < images.count else { return }
|
||||
onImageTap(images, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,35 +301,46 @@ struct AcquisitionOrderCard: View {
|
||||
struct CooperativeSalerCard: View {
|
||||
let acquirer: CooperativeSaler
|
||||
let onDial: (String) -> Void
|
||||
let onEditRemark: () -> 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))
|
||||
ZStack(alignment: .topTrailing) {
|
||||
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
|
||||
)
|
||||
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(.leading, 12)
|
||||
.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)
|
||||
|
||||
Button("修改备注") {
|
||||
onEditRemark()
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(CooperationOrderColors.primary)
|
||||
.padding(.top, 16)
|
||||
.padding(.trailing, 14)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user