重建订单列表 UI 并严格对齐 Android 订单卡片
This commit is contained in:
44
suixinkan/Theme/Modules/OrderTokens.swift
Normal file
44
suixinkan/Theme/Modules/OrderTokens.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// OrderTokens.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreGraphics
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单模块视觉令牌,对齐 Android `OrderListScreen` / `DepositOrderListScreen` inline 色值。
|
||||||
|
enum OrderTokens {
|
||||||
|
|
||||||
|
// MARK: - Colors
|
||||||
|
|
||||||
|
static let filterBackground = UIColor(hex: 0xF4F4F4)
|
||||||
|
static let searchPlaceholder = UIColor(hex: 0xB6BECA)
|
||||||
|
static let label563 = UIColor(hex: 0x4B5563)
|
||||||
|
static let timestamp = UIColor(hex: 0x6B7280)
|
||||||
|
static let orderNumber = UIColor.black
|
||||||
|
static let chipBackground = UIColor(hex: 0xEFF6FF)
|
||||||
|
static let phoneIconBackground = UIColor(hex: 0xEFF6FF)
|
||||||
|
static let giftBorder = UIColor(hex: 0xE5E7EB)
|
||||||
|
static let sourceBorder = UIColor(hex: 0xE6EAF0)
|
||||||
|
static let sourceBackground = UIColor(hex: 0xF9FAFC)
|
||||||
|
static let depositSuccess = UIColor(hex: 0x27AE60)
|
||||||
|
static let depositSuccessBackground = UIColor(hex: 0xE8F8F0)
|
||||||
|
static let depositRefunded = UIColor(hex: 0x6B7280)
|
||||||
|
static let depositRefundedBackground = UIColor(hex: 0xF3F4F6)
|
||||||
|
static let remarkDivider = UIColor(hex: 0xF4F4F4)
|
||||||
|
static let valueBlack = UIColor.black
|
||||||
|
|
||||||
|
// MARK: - Layout
|
||||||
|
|
||||||
|
static let searchHeight: CGFloat = 36
|
||||||
|
static let filterPillHeight: CGFloat = 35
|
||||||
|
static let actionButtonHeight: CGFloat = 48
|
||||||
|
static let cardRadius: CGFloat = 12
|
||||||
|
static let pillRadius: CGFloat = 4
|
||||||
|
static let cardPadding: CGFloat = 16
|
||||||
|
static let cardGap: CGFloat = 12
|
||||||
|
static let filterGap: CGFloat = 8
|
||||||
|
static let infoRowSpacing: CGFloat = 8
|
||||||
|
static let dividerHeight: CGFloat = 0.5
|
||||||
|
static let refundButtonWidthRatio: CGFloat = 0.3
|
||||||
|
}
|
||||||
@ -18,23 +18,19 @@ final class AppStatusBadge: UIView {
|
|||||||
case neutral
|
case neutral
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 订单 badge 字号与内边距样式。
|
||||||
|
enum OrderBadgeStyle {
|
||||||
|
case photographer
|
||||||
|
case deposit
|
||||||
|
}
|
||||||
|
|
||||||
private let label = UILabel()
|
private let label = UILabel()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
AppFont.captionMedium.apply(to: label)
|
|
||||||
label.textAlignment = .center
|
label.textAlignment = .center
|
||||||
addSubview(label)
|
addSubview(label)
|
||||||
label.snp.makeConstraints { make in
|
applyOrderBadgeStyle(.photographer)
|
||||||
make.edges.equalToSuperview().inset(
|
|
||||||
UIEdgeInsets(
|
|
||||||
top: AppSpacing.xxs,
|
|
||||||
left: AppSpacing.xs,
|
|
||||||
bottom: AppSpacing.xxs,
|
|
||||||
right: AppSpacing.xs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
layer.cornerRadius = AppRadius.xs
|
layer.cornerRadius = AppRadius.xs
|
||||||
clipsToBounds = true
|
clipsToBounds = true
|
||||||
}
|
}
|
||||||
@ -44,6 +40,26 @@ final class AppStatusBadge: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置订单 badge 样式(摄影师 14sp / 店铺 12sp)。
|
||||||
|
func applyOrderBadgeStyle(_ style: OrderBadgeStyle) {
|
||||||
|
switch style {
|
||||||
|
case .photographer:
|
||||||
|
label.font = .app(.bodyMedium)
|
||||||
|
label.snp.remakeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: 0, left: AppSpacing.xxs, bottom: 0, right: AppSpacing.xxs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case .deposit:
|
||||||
|
label.font = .app(.caption)
|
||||||
|
label.snp.remakeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: 2, left: AppSpacing.xs, bottom: 2, right: AppSpacing.xs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 按语义色调与文案刷新展示。
|
/// 按语义色调与文案刷新展示。
|
||||||
func apply(tone: Tone, text: String) {
|
func apply(tone: Tone, text: String) {
|
||||||
label.text = text
|
label.text = text
|
||||||
@ -61,15 +77,16 @@ final class AppStatusBadge: UIView {
|
|||||||
label.textColor = AppColor.danger
|
label.textColor = AppColor.danger
|
||||||
backgroundColor = AppColor.dangerBackground
|
backgroundColor = AppColor.dangerBackground
|
||||||
case .neutral:
|
case .neutral:
|
||||||
label.textColor = AppColor.textTabInactive
|
label.textColor = OrderTokens.depositRefunded
|
||||||
backgroundColor = AppColor.inputBackground
|
backgroundColor = OrderTokens.depositRefundedBackground
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 订单状态映射(对齐 Android OrderView)。
|
/// 摄影师订单状态映射(对齐 Android `OrderView`)。
|
||||||
func applyOrderStatus(_ status: Int, text: String) {
|
func applyOrderStatus(_ status: Int, text: String) {
|
||||||
|
applyOrderBadgeStyle(.photographer)
|
||||||
switch status {
|
switch status {
|
||||||
case 18, 20:
|
case 18:
|
||||||
apply(tone: .info, text: text)
|
apply(tone: .info, text: text)
|
||||||
case 30:
|
case 30:
|
||||||
apply(tone: .success, text: text)
|
apply(tone: .success, text: text)
|
||||||
@ -79,4 +96,21 @@ final class AppStatusBadge: UIView {
|
|||||||
apply(tone: .warning, text: text)
|
apply(tone: .warning, text: text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 店铺订单状态映射(对齐 Android `DepositOrderCard`)。
|
||||||
|
func applyDepositOrderStatus(_ status: Int, text: String) {
|
||||||
|
applyOrderBadgeStyle(.deposit)
|
||||||
|
switch status {
|
||||||
|
case 20:
|
||||||
|
apply(tone: .info, text: text)
|
||||||
|
case 30:
|
||||||
|
label.text = text
|
||||||
|
label.textColor = OrderTokens.depositSuccess
|
||||||
|
backgroundColor = OrderTokens.depositSuccessBackground
|
||||||
|
case 50:
|
||||||
|
apply(tone: .neutral, text: text)
|
||||||
|
default:
|
||||||
|
apply(tone: .warning, text: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,29 +10,35 @@ struct DepositOrderCardActions {
|
|||||||
var onRefund: (() -> Void)?
|
var onRefund: (() -> Void)?
|
||||||
var onPartialRefund: (() -> Void)?
|
var onPartialRefund: (() -> Void)?
|
||||||
var onVerify: (() -> Void)?
|
var onVerify: (() -> Void)?
|
||||||
|
var onCallPhone: (() -> Void)?
|
||||||
var onTap: (() -> Void)?
|
var onTap: (() -> Void)?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 店铺订单卡片,对齐 Android `DepositOrderListScreen`。
|
/// 店铺订单卡片,严格对齐 Android `DepositOrderCard`。
|
||||||
final class DepositOrderCardCell: UITableViewCell {
|
final class DepositOrderCardCell: UITableViewCell {
|
||||||
static let reuseIdentifier = "DepositOrderCardCell"
|
static let reuseIdentifier = "DepositOrderCardCell"
|
||||||
|
|
||||||
private let cardView = UIView()
|
private let cardView = UIView()
|
||||||
private let projectLabel = UILabel()
|
private let contentStack = UIStackView()
|
||||||
private let statusBadge = OrderStatusBadgeView()
|
private let headerRow = UIStackView()
|
||||||
private let amountLabel = UILabel()
|
private let orderNumberLabel = UILabel()
|
||||||
private let timeLabel = UILabel()
|
private let statusBadge = DepositOrderStatusBadgeView()
|
||||||
private let uidLabel = UILabel()
|
private let createdAtLabel = UILabel()
|
||||||
private let phoneLabel = UILabel()
|
private let typeChip = OrderTypeChipView()
|
||||||
private let refundButton = UIButton(type: .system)
|
private let infoStack = UIStackView()
|
||||||
private let partialRefundButton = UIButton(type: .system)
|
private let phoneRow = OrderPhoneRowView(style: .deposit)
|
||||||
private let verifyButton = UIButton(type: .system)
|
private let actionRow = UIStackView()
|
||||||
|
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
|
||||||
|
private let partialRefundButton = OrderActionButton(title: "部分退款", style: .dangerFilled)
|
||||||
|
private let verifyButton = OrderActionButton(title: "核销", style: .primaryFilled)
|
||||||
|
|
||||||
private var actions = DepositOrderCardActions()
|
private var actions = DepositOrderCardActions()
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
selectionStyle = .none
|
selectionStyle = .none
|
||||||
backgroundColor = .clear
|
backgroundColor = .clear
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
setupUI()
|
setupUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,98 +49,76 @@ final class DepositOrderCardCell: UITableViewCell {
|
|||||||
|
|
||||||
func configure(item: StoreOrderItem, actions: DepositOrderCardActions) {
|
func configure(item: StoreOrderItem, actions: DepositOrderCardActions) {
|
||||||
self.actions = actions
|
self.actions = actions
|
||||||
projectLabel.text = item.projectName.isEmpty ? item.orderTypeLabel : item.projectName
|
orderNumberLabel.text = "订单号: \(item.orderNumber)"
|
||||||
statusBadge.apply(status: item.orderStatus, text: item.orderStatusName)
|
statusBadge.apply(status: item.orderStatus, text: item.orderStatusName)
|
||||||
amountLabel.text = "¥\(item.actualPayAmount)"
|
createdAtLabel.text = item.createdAt
|
||||||
timeLabel.text = item.createdAt
|
typeChip.apply(text: item.orderTypeLabel)
|
||||||
uidLabel.text = "UID:\(item.userId)"
|
|
||||||
phoneLabel.text = OrderPhoneMask.mask(item.phone)
|
infoStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
addInfoRow(title: "项目名称", value: item.projectName)
|
||||||
|
addInfoRow(title: "订单金额", value: "¥ \(item.actualPayAmount)", primary: true)
|
||||||
|
addInfoRow(title: "付款时间", value: item.payTime.isEmpty ? "--" : item.payTime)
|
||||||
|
addInfoRow(title: "完成时间", value: item.completeTime.isEmpty ? "--" : item.completeTime)
|
||||||
|
addInfoRow(title: "用户 UID", value: String(item.userId))
|
||||||
|
infoStack.addArrangedSubview(phoneRow)
|
||||||
|
phoneRow.apply(phone: item.phone)
|
||||||
|
phoneRow.onTap = { [weak self] in self?.actions.onCallPhone?() }
|
||||||
|
|
||||||
let showVerify = item.orderType == StoreOrderType.offlineProduct && item.orderStatus == 20
|
let showVerify = item.orderType == StoreOrderType.offlineProduct && item.orderStatus == 20
|
||||||
verifyButton.isHidden = !showVerify
|
verifyButton.isHidden = !showVerify
|
||||||
refundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
|
|
||||||
partialRefundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
cardView.backgroundColor = .white
|
cardView.backgroundColor = AppColor.cardBackground
|
||||||
cardView.layer.cornerRadius = 12
|
cardView.layer.cornerRadius = OrderTokens.cardRadius
|
||||||
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
|
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
|
||||||
cardView.addGestureRecognizer(tap)
|
cardView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
projectLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
orderNumberLabel.font = .app(.body)
|
||||||
amountLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
orderNumberLabel.textColor = OrderTokens.orderNumber
|
||||||
amountLabel.textColor = AppColor.primary
|
createdAtLabel.font = .app(.caption)
|
||||||
[timeLabel, uidLabel, phoneLabel].forEach {
|
createdAtLabel.textColor = OrderTokens.timestamp
|
||||||
$0.font = .systemFont(ofSize: 13)
|
|
||||||
$0.textColor = UIColor(hex: 0x6B7280)
|
|
||||||
}
|
|
||||||
|
|
||||||
configureActionButton(refundButton, title: "退款", color: UIColor(hex: 0xEF4444), bg: UIColor(hex: 0xFFE7E7))
|
headerRow.axis = .horizontal
|
||||||
configureActionButton(partialRefundButton, title: "部分退款", color: AppColor.primary, bg: UIColor(hex: 0xEFF6FF))
|
headerRow.alignment = .center
|
||||||
configureActionButton(verifyButton, title: "线下核销", color: .white, bg: UIColor(hex: 0x0073FF))
|
headerRow.distribution = .equalSpacing
|
||||||
|
headerRow.addArrangedSubview(orderNumberLabel)
|
||||||
|
headerRow.addArrangedSubview(statusBadge)
|
||||||
|
|
||||||
|
infoStack.axis = .vertical
|
||||||
|
infoStack.spacing = AppSpacing.xxs
|
||||||
|
|
||||||
|
actionRow.axis = .horizontal
|
||||||
|
actionRow.spacing = OrderTokens.cardGap
|
||||||
|
actionRow.distribution = .fillEqually
|
||||||
|
actionRow.addArrangedSubview(refundButton)
|
||||||
|
actionRow.addArrangedSubview(partialRefundButton)
|
||||||
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
||||||
partialRefundButton.addTarget(self, action: #selector(partialRefundTapped), for: .touchUpInside)
|
partialRefundButton.addTarget(self, action: #selector(partialRefundTapped), for: .touchUpInside)
|
||||||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||||||
|
|
||||||
contentView.addSubview(cardView)
|
contentStack.axis = .vertical
|
||||||
[projectLabel, statusBadge, amountLabel, timeLabel, uidLabel, phoneLabel, refundButton, partialRefundButton, verifyButton].forEach {
|
contentStack.spacing = OrderTokens.cardGap
|
||||||
cardView.addSubview($0)
|
[headerRow, createdAtLabel, typeChip, infoStack, actionRow, verifyButton].forEach {
|
||||||
|
contentStack.addArrangedSubview($0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contentView.addSubview(cardView)
|
||||||
|
cardView.addSubview(contentStack)
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
projectLabel.snp.makeConstraints { make in
|
contentStack.snp.makeConstraints { make in
|
||||||
make.top.leading.equalToSuperview().inset(16)
|
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
|
||||||
}
|
|
||||||
statusBadge.snp.makeConstraints { make in
|
|
||||||
make.centerY.equalTo(projectLabel)
|
|
||||||
make.trailing.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
amountLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(projectLabel.snp.bottom).offset(8)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
timeLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(amountLabel.snp.bottom).offset(4)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
uidLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(timeLabel.snp.bottom).offset(4)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
phoneLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(uidLabel.snp.bottom).offset(4)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
refundButton.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(phoneLabel.snp.bottom).offset(12)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
make.height.equalTo(40)
|
|
||||||
make.width.equalTo(88)
|
|
||||||
make.bottom.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
partialRefundButton.snp.makeConstraints { make in
|
|
||||||
make.centerY.equalTo(refundButton)
|
|
||||||
make.leading.equalTo(refundButton.snp.trailing).offset(8)
|
|
||||||
make.height.equalTo(40)
|
|
||||||
make.width.equalTo(100)
|
|
||||||
}
|
|
||||||
verifyButton.snp.makeConstraints { make in
|
|
||||||
make.centerY.equalTo(refundButton)
|
|
||||||
make.trailing.equalToSuperview().inset(16)
|
|
||||||
make.height.equalTo(40)
|
|
||||||
make.width.equalTo(100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
|
private func addInfoRow(title: String, value: String, primary: Bool = false) {
|
||||||
button.setTitle(title, for: .normal)
|
let row = OrderInfoRowView()
|
||||||
button.setTitleColor(color, for: .normal)
|
row.apply(title: title, value: value, usesPrimaryValue: primary)
|
||||||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
infoStack.addArrangedSubview(row)
|
||||||
button.backgroundColor = bg
|
|
||||||
button.layer.cornerRadius = 8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func refundTapped() { actions.onRefund?() }
|
@objc private func refundTapped() { actions.onRefund?() }
|
||||||
|
|||||||
@ -22,27 +22,43 @@ struct OrderCardActions {
|
|||||||
var onOrderSourceTap: (() -> Void)?
|
var onOrderSourceTap: (() -> Void)?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 摄影师订单卡片,对齐 Android `OrderView`。
|
/// 摄影师订单卡片,严格对齐 Android `OrderView`。
|
||||||
final class OrderCardCell: UITableViewCell {
|
final class OrderCardCell: UITableViewCell {
|
||||||
static let reuseIdentifier = "OrderCardCell"
|
static let reuseIdentifier = "OrderCardCell"
|
||||||
|
|
||||||
private let cardView = UIView()
|
private let cardView = UIView()
|
||||||
|
private let contentStack = UIStackView()
|
||||||
|
|
||||||
|
private let headerRow = UIStackView()
|
||||||
private let orderNumberLabel = UILabel()
|
private let orderNumberLabel = UILabel()
|
||||||
private let statusBadge = OrderStatusBadgeView()
|
private let statusBadge = OrderStatusBadgeView()
|
||||||
private let createdAtLabel = UILabel()
|
private let createdAtLabel = UILabel()
|
||||||
private let typeChip = UILabel()
|
private let typeChip = OrderTypeChipView()
|
||||||
private let actionStack = UIStackView()
|
private let orderSourceContainer = UIView()
|
||||||
private let refundButton = UIButton(type: .system)
|
|
||||||
private let verifyButton = UIButton(type: .system)
|
|
||||||
private let uploadTaskButton = UIButton(type: .system)
|
|
||||||
private let infoStack = UIStackView()
|
|
||||||
private let cancelButton = UIButton(type: .system)
|
|
||||||
private let orderSourceButton = UIButton(type: .system)
|
private let orderSourceButton = UIButton(type: .system)
|
||||||
private let historicalButton = UIButton(type: .system)
|
private let actionRow = UIStackView()
|
||||||
private let refinedStack = UIStackView()
|
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
|
||||||
private let refinedNeedButton = UIButton(type: .system)
|
private let verifyButton = OrderActionButton(title: "核销订单", style: .primaryFilled)
|
||||||
private let refinedNoButton = UIButton(type: .system)
|
private let uploadTaskButton = OrderActionButton(title: "上传任务", style: .primaryLight)
|
||||||
|
private let editOptionView = OrderEditOptionView()
|
||||||
|
private let refundAmountRow = OrderInfoRowView()
|
||||||
|
private let detailStack = UIStackView()
|
||||||
|
private let photoCountRow = UIStackView()
|
||||||
|
private let photoCountLabel = UILabel()
|
||||||
|
private let photoValueLabel = UILabel()
|
||||||
|
private let photoGiftStepper = OrderGiftStepperView()
|
||||||
|
private let videoCountRow = UIStackView()
|
||||||
|
private let videoCountLabel = UILabel()
|
||||||
|
private let videoValueLabel = UILabel()
|
||||||
|
private let videoGiftStepper = OrderGiftStepperView()
|
||||||
|
private let remarkSection = UIStackView()
|
||||||
|
private let remarkDivider = UIView()
|
||||||
|
private let remarkTitleLabel = UILabel()
|
||||||
|
private let remarkBodyLabel = UILabel()
|
||||||
|
private let historicalPhotosView = OrderHistoricalPhotosView()
|
||||||
|
private let cancelButton = OrderActionButton(title: "取消订单", style: .primaryFilled)
|
||||||
|
|
||||||
|
private var refundWidthConstraint: Constraint?
|
||||||
private var actions = OrderCardActions()
|
private var actions = OrderCardActions()
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
@ -58,249 +74,266 @@ final class OrderCardCell: UITableViewCell {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(order: OrderEntity, hideWriteActions: Bool, actions: OrderCardActions, showOrderSource: Bool, sourceText: String?) {
|
func configure(
|
||||||
|
order: OrderEntity,
|
||||||
|
hideWriteActions: Bool,
|
||||||
|
actions: OrderCardActions,
|
||||||
|
showOrderSource: Bool,
|
||||||
|
sourceText: String?
|
||||||
|
) {
|
||||||
self.actions = actions
|
self.actions = actions
|
||||||
orderNumberLabel.text = "订单号:\(order.orderNumber)"
|
orderNumberLabel.text = "订单号:\(order.orderNumber)"
|
||||||
statusBadge.apply(status: order.orderStatus, text: order.orderStatusName)
|
statusBadge.apply(status: order.orderStatus, text: order.orderStatusName)
|
||||||
createdAtLabel.text = order.createdAt.isEmpty ? "--" : order.createdAt
|
createdAtLabel.text = order.createdAt.isEmpty ? "--" : order.createdAt
|
||||||
typeChip.text = order.orderTypeLabel
|
typeChip.apply(text: order.orderTypeLabel)
|
||||||
typeChip.isHidden = order.orderTypeLabel.isEmpty
|
|
||||||
|
|
||||||
let showRefund = !hideWriteActions && (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
|
let showRefund = !hideWriteActions && (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
|
||||||
let showVerify = !hideWriteActions && (
|
let showVerify = !hideWriteActions && (
|
||||||
(order.photoTravel?.needCheckIn == true && order.orderStatus != 30) || order.orderType == 19
|
(order.photoTravel?.needCheckIn == true && order.orderStatus != 30) || order.orderType == 19
|
||||||
)
|
)
|
||||||
|
actionRow.isHidden = !showRefund && !showVerify
|
||||||
refundButton.isHidden = !showRefund
|
refundButton.isHidden = !showRefund
|
||||||
verifyButton.isHidden = !showVerify
|
verifyButton.isHidden = !showVerify
|
||||||
actionStack.isHidden = !showRefund && !showVerify
|
updateRefundWidth(showVerify: showVerify && showRefund)
|
||||||
|
|
||||||
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
|
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
|
||||||
|
|
||||||
orderSourceButton.isHidden = !showOrderSource
|
orderSourceContainer.isHidden = !showOrderSource
|
||||||
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
|
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
|
||||||
|
|
||||||
let showRefined = order.orderStatus == 18 || order.orderStatus == 30
|
let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
|
||||||
refinedStack.isHidden = !showRefined
|
editOptionView.isHidden = !showEdit
|
||||||
updateRefinedButtons(isRefined: order.isRefined, readOnly: hideWriteActions)
|
if showEdit {
|
||||||
|
editOptionView.apply(isRefined: order.isRefined, readOnly: hideWriteActions)
|
||||||
cancelButton.isHidden = hideWriteActions || order.photoTravel?.canCancelOrder != true
|
|
||||||
|
|
||||||
infoStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
|
||||||
addInfoRow(title: "付款金额", value: "¥\(order.actualPayAmount)")
|
|
||||||
if !order.payTime.isEmpty { addInfoRow(title: "付款时间", value: order.payTime) }
|
|
||||||
if !order.completeTime.isEmpty { addInfoRow(title: "完成时间", value: order.completeTime) }
|
|
||||||
if !order.payTypeName.isEmpty { addInfoRow(title: "付款方式", value: order.payTypeName) }
|
|
||||||
addInfoRow(title: "UID", value: String(order.userId))
|
|
||||||
addPhoneRow(phone: order.phone, hidden: hideWriteActions)
|
|
||||||
if let travel = order.photoTravel, !travel.checkInTime.isEmpty {
|
|
||||||
addInfoRow(title: "核销时间", value: travel.checkInTime)
|
|
||||||
}
|
}
|
||||||
if let travel = order.photoTravel {
|
|
||||||
addInfoRow(title: "修图张数", value: "\(travel.orderPhotoNum)")
|
let showRefundAmount = order.orderStatus == 50
|
||||||
addInfoRow(title: "视频", value: "\(travel.orderVideoNum)")
|
&& !order.actualRefundAmount.isEmpty
|
||||||
|
&& order.actualRefundAmount != "0"
|
||||||
|
refundAmountRow.isHidden = !showRefundAmount
|
||||||
|
if showRefundAmount {
|
||||||
|
refundAmountRow.apply(title: "退款金额", value: "¥\(order.actualRefundAmount)", usesPrimaryValue: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rebuildDetailRows(order: order, hideWriteActions: hideWriteActions)
|
||||||
|
configureTravelRows(order: order, hideWriteActions: hideWriteActions)
|
||||||
|
|
||||||
if let remark = order.remark, !remark.isEmpty {
|
if let remark = order.remark, !remark.isEmpty {
|
||||||
addInfoRow(title: "备注", value: remark)
|
remarkSection.isHidden = false
|
||||||
|
remarkBodyLabel.text = remark
|
||||||
|
} else {
|
||||||
|
remarkSection.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let materials = order.multiTravel?.materialList ?? []
|
let materials = order.multiTravel?.materialList ?? []
|
||||||
historicalButton.isHidden = materials.isEmpty
|
historicalPhotosView.isHidden = materials.isEmpty
|
||||||
|
if !materials.isEmpty {
|
||||||
|
historicalPhotosView.apply(materials: materials)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelButton.isHidden = hideWriteActions || order.photoTravel?.canCancelOrder != true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
cardView.backgroundColor = AppColor.cardBackground
|
cardView.backgroundColor = AppColor.cardBackground
|
||||||
cardView.layer.cornerRadius = AppRadius.lg
|
cardView.layer.cornerRadius = OrderTokens.cardRadius
|
||||||
|
|
||||||
orderNumberLabel.font = .app(.body)
|
orderNumberLabel.font = .app(.body)
|
||||||
orderNumberLabel.textColor = AppColor.textPrimary
|
orderNumberLabel.textColor = OrderTokens.orderNumber
|
||||||
createdAtLabel.font = .app(.caption)
|
createdAtLabel.font = .app(.caption)
|
||||||
createdAtLabel.textColor = AppColor.textSecondary
|
createdAtLabel.textColor = OrderTokens.timestamp
|
||||||
typeChip.font = .app(.caption)
|
|
||||||
typeChip.textColor = AppColor.primary
|
|
||||||
typeChip.backgroundColor = AppColor.primaryLight
|
|
||||||
typeChip.layer.cornerRadius = AppRadius.xs
|
|
||||||
typeChip.clipsToBounds = true
|
|
||||||
typeChip.textAlignment = .center
|
|
||||||
|
|
||||||
configureActionButton(refundButton, title: "退款", color: AppColor.danger, bg: AppColor.dangerBackground)
|
headerRow.axis = .horizontal
|
||||||
configureActionButton(verifyButton, title: "核销订单", color: .white, bg: AppColor.primary)
|
headerRow.alignment = .center
|
||||||
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
headerRow.distribution = .equalSpacing
|
||||||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
headerRow.addArrangedSubview(orderNumberLabel)
|
||||||
|
headerRow.addArrangedSubview(statusBadge)
|
||||||
uploadTaskButton.setTitle("上传任务", for: .normal)
|
|
||||||
uploadTaskButton.setTitleColor(AppColor.primary, for: .normal)
|
|
||||||
uploadTaskButton.titleLabel?.font = .app(.subtitle)
|
|
||||||
uploadTaskButton.backgroundColor = AppColor.primaryLight
|
|
||||||
uploadTaskButton.layer.cornerRadius = AppRadius.lg
|
|
||||||
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
actionStack.axis = .horizontal
|
|
||||||
actionStack.spacing = AppSpacing.sm
|
|
||||||
actionStack.distribution = .fillEqually
|
|
||||||
actionStack.addArrangedSubview(refundButton)
|
|
||||||
actionStack.addArrangedSubview(verifyButton)
|
|
||||||
|
|
||||||
infoStack.axis = .vertical
|
|
||||||
infoStack.spacing = 6
|
|
||||||
|
|
||||||
cancelButton.setTitle("取消订单", for: .normal)
|
|
||||||
cancelButton.setTitleColor(AppColor.danger, for: .normal)
|
|
||||||
cancelButton.titleLabel?.font = .app(.body)
|
|
||||||
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
|
orderSourceContainer.backgroundColor = OrderTokens.sourceBackground
|
||||||
|
orderSourceContainer.layer.cornerRadius = AppRadius.sm
|
||||||
|
orderSourceContainer.layer.borderWidth = 1
|
||||||
|
orderSourceContainer.layer.borderColor = OrderTokens.sourceBorder.cgColor
|
||||||
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
|
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
|
||||||
orderSourceButton.titleLabel?.font = .app(.body)
|
orderSourceButton.titleLabel?.font = .app(.body)
|
||||||
orderSourceButton.contentHorizontalAlignment = .leading
|
orderSourceButton.contentHorizontalAlignment = .leading
|
||||||
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
|
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
|
||||||
|
orderSourceContainer.addSubview(orderSourceButton)
|
||||||
historicalButton.setTitle("历史拍摄", for: .normal)
|
orderSourceButton.snp.makeConstraints { make in
|
||||||
historicalButton.setTitleColor(AppColor.primary, for: .normal)
|
make.edges.equalToSuperview().inset(AppSpacing.sm)
|
||||||
historicalButton.titleLabel?.font = .app(.body)
|
|
||||||
historicalButton.addTarget(self, action: #selector(historicalTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
refinedStack.axis = .horizontal
|
|
||||||
refinedStack.spacing = AppSpacing.xs
|
|
||||||
refinedNeedButton.setTitle("需要精修", for: .normal)
|
|
||||||
refinedNoButton.setTitle("无需精修", for: .normal)
|
|
||||||
[refinedNeedButton, refinedNoButton].forEach {
|
|
||||||
$0.titleLabel?.font = .app(.body)
|
|
||||||
$0.layer.cornerRadius = AppRadius.sm
|
|
||||||
$0.layer.borderWidth = 1
|
|
||||||
}
|
}
|
||||||
refinedNeedButton.addTarget(self, action: #selector(refinedNeedTapped), for: .touchUpInside)
|
|
||||||
refinedNoButton.addTarget(self, action: #selector(refinedNoTapped), for: .touchUpInside)
|
actionRow.axis = .horizontal
|
||||||
refinedStack.addArrangedSubview(refinedNeedButton)
|
actionRow.spacing = OrderTokens.cardGap
|
||||||
refinedStack.addArrangedSubview(refinedNoButton)
|
actionRow.alignment = .fill
|
||||||
|
actionRow.addArrangedSubview(refundButton)
|
||||||
|
actionRow.addArrangedSubview(verifyButton)
|
||||||
|
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
||||||
|
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
editOptionView.onSelectionChange = { [weak self] value in
|
||||||
|
self?.actions.onRefinedChanged?(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
detailStack.axis = .vertical
|
||||||
|
detailStack.spacing = OrderTokens.infoRowSpacing
|
||||||
|
|
||||||
|
configureTravelRow(photoCountRow, titleLabel: photoCountLabel, valueLabel: photoValueLabel, title: "修图张数", stepper: photoGiftStepper)
|
||||||
|
configureTravelRow(videoCountRow, titleLabel: videoCountLabel, valueLabel: videoValueLabel, title: "视频剪辑", stepper: videoGiftStepper)
|
||||||
|
photoGiftStepper.onMinusTap = { [weak self] in self?.actions.onSendGift?() }
|
||||||
|
photoGiftStepper.onPlusTap = { [weak self] in self?.actions.onSendGift?() }
|
||||||
|
videoGiftStepper.onMinusTap = { [weak self] in self?.actions.onSendGift?() }
|
||||||
|
videoGiftStepper.onPlusTap = { [weak self] in self?.actions.onSendGift?() }
|
||||||
|
|
||||||
|
remarkDivider.backgroundColor = OrderTokens.remarkDivider
|
||||||
|
remarkTitleLabel.text = "备注信息:"
|
||||||
|
remarkTitleLabel.font = .app(.body)
|
||||||
|
remarkTitleLabel.textColor = OrderTokens.label563
|
||||||
|
remarkBodyLabel.font = .app(.body)
|
||||||
|
remarkBodyLabel.textColor = OrderTokens.label563
|
||||||
|
remarkBodyLabel.numberOfLines = 0
|
||||||
|
remarkSection.axis = .vertical
|
||||||
|
remarkSection.spacing = 6
|
||||||
|
remarkSection.addArrangedSubview(remarkDivider)
|
||||||
|
remarkSection.addArrangedSubview(remarkTitleLabel)
|
||||||
|
remarkSection.addArrangedSubview(remarkBodyLabel)
|
||||||
|
remarkDivider.snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
historicalPhotosView.onTap = { [weak self] in self?.actions.onHistoricalPhoto?() }
|
||||||
|
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
contentStack.axis = .vertical
|
||||||
|
contentStack.spacing = OrderTokens.cardGap
|
||||||
|
[
|
||||||
|
headerRow,
|
||||||
|
createdAtLabel,
|
||||||
|
typeChip,
|
||||||
|
orderSourceContainer,
|
||||||
|
actionRow,
|
||||||
|
uploadTaskButton,
|
||||||
|
editOptionView,
|
||||||
|
refundAmountRow,
|
||||||
|
detailStack,
|
||||||
|
photoCountRow,
|
||||||
|
videoCountRow,
|
||||||
|
remarkSection,
|
||||||
|
historicalPhotosView,
|
||||||
|
cancelButton,
|
||||||
|
].forEach { contentStack.addArrangedSubview($0) }
|
||||||
|
|
||||||
contentView.addSubview(cardView)
|
contentView.addSubview(cardView)
|
||||||
cardView.addSubview(orderNumberLabel)
|
cardView.addSubview(contentStack)
|
||||||
cardView.addSubview(statusBadge)
|
|
||||||
cardView.addSubview(createdAtLabel)
|
|
||||||
cardView.addSubview(typeChip)
|
|
||||||
cardView.addSubview(orderSourceButton)
|
|
||||||
cardView.addSubview(actionStack)
|
|
||||||
cardView.addSubview(uploadTaskButton)
|
|
||||||
cardView.addSubview(refinedStack)
|
|
||||||
cardView.addSubview(infoStack)
|
|
||||||
cardView.addSubview(historicalButton)
|
|
||||||
cardView.addSubview(cancelButton)
|
|
||||||
|
|
||||||
cardView.snp.makeConstraints { make in
|
cardView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: AppSpacing.sm, right: AppSpacing.md))
|
make.edges.equalToSuperview().inset(
|
||||||
|
UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
orderNumberLabel.snp.makeConstraints { make in
|
contentStack.snp.makeConstraints { make in
|
||||||
make.top.leading.equalToSuperview().inset(16)
|
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
|
||||||
}
|
|
||||||
statusBadge.snp.makeConstraints { make in
|
|
||||||
make.centerY.equalTo(orderNumberLabel)
|
|
||||||
make.trailing.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
createdAtLabel.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(orderNumberLabel.snp.bottom).offset(4)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
typeChip.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(createdAtLabel.snp.bottom).offset(4)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
make.height.equalTo(20)
|
|
||||||
make.width.greaterThanOrEqualTo(40)
|
|
||||||
}
|
|
||||||
orderSourceButton.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(typeChip.snp.bottom).offset(8)
|
|
||||||
make.leading.trailing.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
actionStack.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(orderSourceButton.snp.bottom).offset(12)
|
|
||||||
make.leading.trailing.equalToSuperview().inset(16)
|
|
||||||
make.height.equalTo(48)
|
|
||||||
}
|
|
||||||
uploadTaskButton.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(actionStack.snp.bottom).offset(12)
|
|
||||||
make.leading.trailing.equalToSuperview().inset(16)
|
|
||||||
make.height.equalTo(48)
|
|
||||||
}
|
|
||||||
refinedStack.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(uploadTaskButton.snp.bottom).offset(12)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
infoStack.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(refinedStack.snp.bottom).offset(12)
|
|
||||||
make.leading.trailing.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
historicalButton.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(infoStack.snp.bottom).offset(8)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
}
|
|
||||||
cancelButton.snp.makeConstraints { make in
|
|
||||||
make.top.equalTo(historicalButton.snp.bottom).offset(12)
|
|
||||||
make.leading.equalToSuperview().inset(16)
|
|
||||||
make.bottom.equalToSuperview().inset(16)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
|
private func configureTravelRow(
|
||||||
button.setTitle(title, for: .normal)
|
_ row: UIStackView,
|
||||||
button.setTitleColor(color, for: .normal)
|
titleLabel: UILabel,
|
||||||
button.titleLabel?.font = .app(.subtitle)
|
valueLabel: UILabel,
|
||||||
button.backgroundColor = bg
|
title: String,
|
||||||
button.layer.cornerRadius = AppRadius.sm
|
stepper: OrderGiftStepperView
|
||||||
}
|
) {
|
||||||
|
|
||||||
private func addInfoRow(title: String, value: String) {
|
|
||||||
let row = UIStackView()
|
|
||||||
row.axis = .horizontal
|
row.axis = .horizontal
|
||||||
row.distribution = .fillEqually
|
row.alignment = .center
|
||||||
let titleLabel = UILabel()
|
row.distribution = .equalSpacing
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
titleLabel.font = .app(.body)
|
titleLabel.font = .app(.body)
|
||||||
titleLabel.textColor = AppColor.textSecondary
|
titleLabel.textColor = OrderTokens.label563
|
||||||
let valueLabel = UILabel()
|
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
valueLabel.text = value
|
valueLabel.textColor = OrderTokens.valueBlack
|
||||||
valueLabel.font = .app(.body)
|
|
||||||
valueLabel.textColor = AppColor.textPrimary
|
|
||||||
valueLabel.textAlignment = .right
|
|
||||||
row.addArrangedSubview(titleLabel)
|
row.addArrangedSubview(titleLabel)
|
||||||
|
row.addArrangedSubview(stepper)
|
||||||
row.addArrangedSubview(valueLabel)
|
row.addArrangedSubview(valueLabel)
|
||||||
infoStack.addArrangedSubview(row)
|
valueLabel.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addPhoneRow(phone: String, hidden: Bool) {
|
private func updateRefundWidth(showVerify: Bool) {
|
||||||
let row = UIStackView()
|
refundWidthConstraint?.deactivate()
|
||||||
row.axis = .horizontal
|
if showVerify {
|
||||||
row.distribution = .fillEqually
|
refundButton.snp.makeConstraints { make in
|
||||||
let titleLabel = UILabel()
|
refundWidthConstraint = make.width.equalTo(actionRow.snp.width).multipliedBy(OrderTokens.refundButtonWidthRatio).constraint
|
||||||
titleLabel.text = "手机号"
|
}
|
||||||
titleLabel.font = .app(.body)
|
}
|
||||||
titleLabel.textColor = AppColor.textSecondary
|
|
||||||
let valueButton = UIButton(type: .system)
|
|
||||||
valueButton.setTitle(OrderPhoneMask.mask(phone), for: .normal)
|
|
||||||
valueButton.setTitleColor(AppColor.primary, for: .normal)
|
|
||||||
valueButton.titleLabel?.font = .app(.body)
|
|
||||||
valueButton.contentHorizontalAlignment = .trailing
|
|
||||||
valueButton.isHidden = hidden || phone.isEmpty
|
|
||||||
valueButton.addTarget(self, action: #selector(callTapped), for: .touchUpInside)
|
|
||||||
row.addArrangedSubview(titleLabel)
|
|
||||||
row.addArrangedSubview(valueButton)
|
|
||||||
infoStack.addArrangedSubview(row)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRefinedButtons(isRefined: Int, readOnly: Bool) {
|
private func rebuildDetailRows(order: OrderEntity, hideWriteActions: Bool) {
|
||||||
let selectedBorder = AppColor.primary.cgColor
|
detailStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
let normalBorder = AppColor.border.cgColor
|
|
||||||
refinedNeedButton.layer.borderColor = isRefined == 1 ? selectedBorder : normalBorder
|
addDetailRow(title: "付款金额", value: "¥\(order.actualPayAmount)", primary: true)
|
||||||
refinedNoButton.layer.borderColor = isRefined == 2 ? selectedBorder : normalBorder
|
addDetailRow(title: "付款时间", value: resolvedPayTime(order))
|
||||||
refinedNeedButton.isEnabled = !readOnly
|
addDetailRow(title: "完成时间", value: order.completeTime.isEmpty ? "--" : order.completeTime)
|
||||||
refinedNoButton.isEnabled = !readOnly
|
if !order.payTypeName.isEmpty {
|
||||||
|
addDetailRow(title: "付款方式", value: order.payTypeName)
|
||||||
|
}
|
||||||
|
addDetailRow(title: "UID", value: String(order.userId))
|
||||||
|
|
||||||
|
let phoneRow = OrderPhoneRowView(style: .photographer)
|
||||||
|
phoneRow.apply(phone: order.phone, hidden: hideWriteActions)
|
||||||
|
phoneRow.onTap = { [weak self] in self?.actions.onCallPhone?() }
|
||||||
|
detailStack.addArrangedSubview(phoneRow)
|
||||||
|
|
||||||
|
if !order.projectName.isEmpty {
|
||||||
|
addDetailRow(title: "关联项目", value: order.projectName)
|
||||||
|
}
|
||||||
|
if let travel = order.photoTravel, !travel.checkInTime.isEmpty {
|
||||||
|
addDetailRow(title: "核销时间", value: travel.checkInTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureTravelRows(order: OrderEntity, hideWriteActions: Bool) {
|
||||||
|
let showTravel = order.photoTravel != nil && order.orderType != 19 && order.orderType != 14
|
||||||
|
photoCountRow.isHidden = !showTravel
|
||||||
|
videoCountRow.isHidden = !showTravel
|
||||||
|
guard showTravel, let travel = order.photoTravel else { return }
|
||||||
|
|
||||||
|
let canGift = travel.canGiftRetouch && !hideWriteActions
|
||||||
|
let photoText = "\(travel.orderPhotoNum + travel.retouchGiftPhotoNum) 张"
|
||||||
|
let videoText = "\(travel.orderVideoNum + travel.retouchGiftVideoNum) 条"
|
||||||
|
|
||||||
|
photoGiftStepper.isHidden = !canGift
|
||||||
|
videoGiftStepper.isHidden = !canGift
|
||||||
|
photoValueLabel.isHidden = canGift
|
||||||
|
videoValueLabel.isHidden = canGift
|
||||||
|
|
||||||
|
if canGift {
|
||||||
|
photoGiftStepper.apply(text: photoText)
|
||||||
|
videoGiftStepper.apply(text: videoText)
|
||||||
|
} else {
|
||||||
|
photoValueLabel.text = photoText
|
||||||
|
videoValueLabel.text = videoText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addDetailRow(title: String, value: String, primary: Bool = false) {
|
||||||
|
let row = OrderInfoRowView()
|
||||||
|
row.apply(title: title, value: value, usesPrimaryValue: primary)
|
||||||
|
detailStack.addArrangedSubview(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedPayTime(_ order: OrderEntity) -> String {
|
||||||
|
let payTime = order.payTime
|
||||||
|
if payTime.isEmpty { return order.depositPayTime.isEmpty ? "--" : order.depositPayTime }
|
||||||
|
if payTime.hasPrefix("1970") || payTime == "0" {
|
||||||
|
return order.depositPayTime.isEmpty ? "--" : order.depositPayTime
|
||||||
|
}
|
||||||
|
return payTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func refundTapped() { actions.onRefund?() }
|
@objc private func refundTapped() { actions.onRefund?() }
|
||||||
@objc private func verifyTapped() { actions.onVerify?() }
|
@objc private func verifyTapped() { actions.onVerify?() }
|
||||||
@objc private func uploadTaskTapped() { actions.onUploadTask?() }
|
@objc private func uploadTaskTapped() { actions.onUploadTask?() }
|
||||||
@objc private func cancelTapped() { actions.onCancel?() }
|
@objc private func cancelTapped() { actions.onCancel?() }
|
||||||
@objc private func callTapped() { actions.onCallPhone?() }
|
|
||||||
@objc private func orderSourceTapped() { actions.onOrderSourceTap?() }
|
@objc private func orderSourceTapped() { actions.onOrderSourceTap?() }
|
||||||
@objc private func historicalTapped() { actions.onHistoricalPhoto?() }
|
|
||||||
@objc private func refinedNeedTapped() { actions.onRefinedChanged?(1) }
|
|
||||||
@objc private func refinedNoTapped() { actions.onRefinedChanged?(2) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OrderPhoneMask {
|
enum OrderPhoneMask {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// 订单筛选栏:搜索 + 状态下拉 + 日期筛选。
|
/// 订单筛选栏:搜索 + 状态下拉 + 日期筛选,对齐 Android Header。
|
||||||
final class OrderFilterBarView: UIView {
|
final class OrderFilterBarView: UIView {
|
||||||
|
|
||||||
var onSearchChanged: ((String) -> Void)?
|
var onSearchChanged: ((String) -> Void)?
|
||||||
@ -14,16 +14,18 @@ final class OrderFilterBarView: UIView {
|
|||||||
var onFilterSelected: ((Int, Int) -> Void)?
|
var onFilterSelected: ((Int, Int) -> Void)?
|
||||||
var onDateRangeTap: (() -> Void)?
|
var onDateRangeTap: (() -> Void)?
|
||||||
|
|
||||||
|
private let searchContainer = UIView()
|
||||||
|
private let searchIconView = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||||
private let searchField = UITextField()
|
private let searchField = UITextField()
|
||||||
private let filterButton = UIButton(type: .system)
|
private let divider = UIView()
|
||||||
private let dateButton = UIButton(type: .system)
|
private let filterPill = OrderFilterPillControl()
|
||||||
|
private let datePill = OrderFilterPillControl()
|
||||||
|
|
||||||
private var isStoreMode = false
|
private var isStoreMode = false
|
||||||
private var currentFilterLabel = "全部"
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = .white
|
backgroundColor = AppColor.cardBackground
|
||||||
setupUI()
|
setupUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,95 +36,102 @@ final class OrderFilterBarView: UIView {
|
|||||||
|
|
||||||
func configure(isStoreMode: Bool, filterLabel: String, dateRangeText: String, searchText: String) {
|
func configure(isStoreMode: Bool, filterLabel: String, dateRangeText: String, searchText: String) {
|
||||||
self.isStoreMode = isStoreMode
|
self.isStoreMode = isStoreMode
|
||||||
currentFilterLabel = filterLabel
|
|
||||||
searchField.placeholder = isStoreMode ? "输入游客手机号" : "输入手机号搜索"
|
searchField.placeholder = isStoreMode ? "输入游客手机号" : "输入手机号搜索"
|
||||||
|
searchField.attributedPlaceholder = NSAttributedString(
|
||||||
|
string: searchField.placeholder ?? "",
|
||||||
|
attributes: [.foregroundColor: OrderTokens.searchPlaceholder]
|
||||||
|
)
|
||||||
searchField.text = searchText
|
searchField.text = searchText
|
||||||
filterButton.setTitle("\(filterLabel) ▾", for: .normal)
|
filterPill.pillTitle = filterLabel
|
||||||
dateButton.setTitle(dateRangeText, for: .normal)
|
datePill.pillTitle = dateRangeText
|
||||||
|
rebuildFilterMenu(selectedLabel: filterLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
searchField.font = .systemFont(ofSize: 14)
|
searchContainer.backgroundColor = OrderTokens.filterBackground
|
||||||
|
searchContainer.layer.cornerRadius = OrderTokens.pillRadius
|
||||||
|
searchContainer.clipsToBounds = true
|
||||||
|
|
||||||
|
searchIconView.tintColor = .gray
|
||||||
|
searchIconView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
searchField.font = .app(.body)
|
||||||
|
searchField.textColor = AppColor.textPrimary
|
||||||
searchField.keyboardType = .phonePad
|
searchField.keyboardType = .phonePad
|
||||||
searchField.returnKeyType = .search
|
searchField.returnKeyType = .search
|
||||||
searchField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
searchField.borderStyle = .none
|
||||||
searchField.layer.cornerRadius = 4
|
searchField.backgroundColor = .clear
|
||||||
searchField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 12))
|
|
||||||
searchField.leftViewMode = .always
|
|
||||||
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
|
|
||||||
searchField.delegate = self
|
searchField.delegate = self
|
||||||
|
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
|
||||||
|
|
||||||
filterButton.setTitleColor(AppColor.text333, for: .normal)
|
divider.backgroundColor = AppColor.border
|
||||||
filterButton.titleLabel?.font = .systemFont(ofSize: 14)
|
|
||||||
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
dateButton.setTitleColor(AppColor.primary, for: .normal)
|
filterPill.showsMenuAsPrimaryAction = true
|
||||||
dateButton.titleLabel?.font = .systemFont(ofSize: 14)
|
|
||||||
dateButton.contentHorizontalAlignment = .trailing
|
|
||||||
dateButton.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
|
|
||||||
|
|
||||||
addSubview(searchField)
|
datePill.setTrailingSystemImage("calendar")
|
||||||
addSubview(filterButton)
|
datePill.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
|
||||||
addSubview(dateButton)
|
|
||||||
|
|
||||||
|
addSubview(searchContainer)
|
||||||
|
searchContainer.addSubview(searchIconView)
|
||||||
|
searchContainer.addSubview(searchField)
|
||||||
|
addSubview(divider)
|
||||||
|
addSubview(filterPill)
|
||||||
|
addSubview(datePill)
|
||||||
|
|
||||||
|
searchContainer.snp.makeConstraints { make in
|
||||||
|
make.top.leading.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.height.equalTo(OrderTokens.searchHeight)
|
||||||
|
}
|
||||||
|
searchIconView.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().inset(AppSpacing.sm)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(18)
|
||||||
|
}
|
||||||
searchField.snp.makeConstraints { make in
|
searchField.snp.makeConstraints { make in
|
||||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
make.leading.equalTo(searchIconView.snp.trailing).offset(10)
|
||||||
make.height.equalTo(36)
|
make.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
}
|
}
|
||||||
filterButton.snp.makeConstraints { make in
|
divider.snp.makeConstraints { make in
|
||||||
make.top.equalTo(searchField.snp.bottom).offset(12)
|
make.top.equalTo(searchContainer.snp.bottom).offset(AppSpacing.md)
|
||||||
make.leading.equalToSuperview().inset(16)
|
make.leading.trailing.equalToSuperview()
|
||||||
make.bottom.equalToSuperview().inset(12)
|
make.height.equalTo(OrderTokens.dividerHeight)
|
||||||
}
|
}
|
||||||
dateButton.snp.makeConstraints { make in
|
filterPill.snp.makeConstraints { make in
|
||||||
make.centerY.equalTo(filterButton)
|
make.top.equalTo(divider.snp.bottom).offset(AppSpacing.md)
|
||||||
make.trailing.equalToSuperview().inset(16)
|
make.leading.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
make.bottom.equalToSuperview().inset(AppSpacing.md)
|
||||||
}
|
}
|
||||||
|
datePill.snp.makeConstraints { make in
|
||||||
|
make.top.bottom.width.equalTo(filterPill)
|
||||||
|
make.leading.equalTo(filterPill.snp.trailing).offset(OrderTokens.filterGap)
|
||||||
|
make.trailing.equalToSuperview().inset(AppSpacing.md)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rebuildFilterMenu(selectedLabel: String) {
|
||||||
|
let items: [(String, Int, Int)] = isStoreMode
|
||||||
|
? StoreOrderFilter.allCases.map { ($0.label, $0.tabIndex, 0) }
|
||||||
|
: PhotographerOrderFilter.allCases.map { ($0.label, $0.tabIndex, $0.rTabIndex) }
|
||||||
|
|
||||||
|
let actions = items.map { title, tab, rTab in
|
||||||
|
UIAction(title: title, state: title == selectedLabel ? .on : .off) { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.filterPill.pillTitle = title
|
||||||
|
self.rebuildFilterMenu(selectedLabel: title)
|
||||||
|
self.onFilterSelected?(tab, rTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filterPill.menu = UIMenu(title: "", options: .displayInline, children: actions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func searchChanged() {
|
@objc private func searchChanged() {
|
||||||
onSearchChanged?(searchField.text ?? "")
|
onSearchChanged?(searchField.text ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filterTapped() {
|
|
||||||
if isStoreMode {
|
|
||||||
presentStoreFilterMenu()
|
|
||||||
} else {
|
|
||||||
presentPhotographerFilterMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func dateTapped() {
|
@objc private func dateTapped() {
|
||||||
onDateRangeTap?()
|
onDateRangeTap?()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentPhotographerFilterMenu() {
|
|
||||||
let actions: [(String, Int, Int)] = PhotographerOrderFilter.allCases.map {
|
|
||||||
($0.label, $0.tabIndex, $0.rTabIndex)
|
|
||||||
}
|
|
||||||
presentMenu(actions: actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentStoreFilterMenu() {
|
|
||||||
let actions: [(String, Int, Int)] = StoreOrderFilter.allCases.map {
|
|
||||||
($0.label, $0.tabIndex, 0)
|
|
||||||
}
|
|
||||||
presentMenu(actions: actions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentMenu(actions: [(String, Int, Int)]) {
|
|
||||||
guard let presenter = window?.rootViewController?.presentedViewController ?? window?.rootViewController else { return }
|
|
||||||
let alert = UIAlertController(title: "筛选订单", message: nil, preferredStyle: .actionSheet)
|
|
||||||
actions.forEach { title, tab, rTab in
|
|
||||||
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
|
||||||
self?.currentFilterLabel = title
|
|
||||||
self?.filterButton.setTitle("\(title) ▾", for: .normal)
|
|
||||||
self?.onFilterSelected?(tab, rTab)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
||||||
presenter.present(alert, animated: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension OrderFilterBarView: UITextFieldDelegate {
|
extension OrderFilterBarView: UITextFieldDelegate {
|
||||||
|
|||||||
@ -6,39 +6,53 @@
|
|||||||
import SnapKit
|
import SnapKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// 订单列表容器,含下拉刷新与分页。
|
/// 订单列表容器,含下拉刷新、空态与分页,对齐 Android LazyColumn。
|
||||||
final class OrderListView: UIView {
|
final class OrderListView: UIView {
|
||||||
|
|
||||||
enum Mode {
|
|
||||||
case photographer
|
|
||||||
case store
|
|
||||||
}
|
|
||||||
|
|
||||||
var onRefresh: (() -> Void)?
|
var onRefresh: (() -> Void)?
|
||||||
var onLoadMore: ((Int) -> Void)?
|
var onLoadMore: ((Int) -> Void)?
|
||||||
|
|
||||||
let tableView = UITableView(frame: .zero, style: .plain)
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
private let refreshControl = UIRefreshControl()
|
private let refreshControl = UIRefreshControl()
|
||||||
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
private let emptyLabel = UILabel()
|
||||||
private let footerLabel = UILabel()
|
private let footerLabel = UILabel()
|
||||||
|
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = UIColor(hex: 0xF5F5F5)
|
backgroundColor = AppColor.pageBackground
|
||||||
tableView.backgroundColor = .clear
|
tableView.backgroundColor = .clear
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
tableView.keyboardDismissMode = .onDrag
|
tableView.keyboardDismissMode = .onDrag
|
||||||
|
tableView.contentInset = UIEdgeInsets(top: OrderTokens.cardGap, left: 0, bottom: OrderTokens.cardGap, right: 0)
|
||||||
tableView.refreshControl = refreshControl
|
tableView.refreshControl = refreshControl
|
||||||
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
||||||
|
|
||||||
footerLabel.font = .systemFont(ofSize: 12)
|
loadingIndicator.hidesWhenStopped = true
|
||||||
footerLabel.textColor = UIColor(hex: 0x9CA3AF)
|
loadingIndicator.color = AppColor.primary
|
||||||
|
|
||||||
|
emptyLabel.font = .app(.body)
|
||||||
|
emptyLabel.textColor = OrderTokens.label563
|
||||||
|
emptyLabel.text = "暂无订单"
|
||||||
|
emptyLabel.textAlignment = .center
|
||||||
|
|
||||||
|
footerLabel.font = .app(.body)
|
||||||
|
footerLabel.textColor = OrderTokens.label563
|
||||||
footerLabel.textAlignment = .center
|
footerLabel.textAlignment = .center
|
||||||
footerLabel.text = "没有更多"
|
footerLabel.text = "没有更多"
|
||||||
|
|
||||||
|
footerSpinner.hidesWhenStopped = true
|
||||||
|
footerSpinner.color = AppColor.primary
|
||||||
|
|
||||||
addSubview(tableView)
|
addSubview(tableView)
|
||||||
|
addSubview(loadingIndicator)
|
||||||
tableView.snp.makeConstraints { make in
|
tableView.snp.makeConstraints { make in
|
||||||
make.edges.equalToSuperview()
|
make.edges.equalToSuperview()
|
||||||
}
|
}
|
||||||
|
loadingIndicator.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@ -46,18 +60,54 @@ final class OrderListView: UIView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(isRefreshing: Bool, canLoadMore: Bool, isEmpty: Bool) {
|
func apply(
|
||||||
|
isRefreshing: Bool,
|
||||||
|
canLoadMore: Bool,
|
||||||
|
isEmpty: Bool,
|
||||||
|
isLoading: Bool = false,
|
||||||
|
isLoadingMore: Bool = false
|
||||||
|
) {
|
||||||
if isRefreshing {
|
if isRefreshing {
|
||||||
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
|
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
|
||||||
} else {
|
} else {
|
||||||
refreshControl.endRefreshing()
|
refreshControl.endRefreshing()
|
||||||
}
|
}
|
||||||
footerLabel.text = isEmpty ? "暂无订单" : (canLoadMore ? "" : "没有更多")
|
|
||||||
tableView.tableFooterView = footerLabelFrame(isEmpty: isEmpty, canLoadMore: canLoadMore)
|
if isLoading, isEmpty {
|
||||||
|
loadingIndicator.startAnimating()
|
||||||
|
tableView.backgroundView = nil
|
||||||
|
tableView.tableFooterView = nil
|
||||||
|
} else {
|
||||||
|
loadingIndicator.stopAnimating()
|
||||||
|
if isEmpty {
|
||||||
|
tableView.backgroundView = emptyBackgroundView()
|
||||||
|
} else {
|
||||||
|
tableView.backgroundView = nil
|
||||||
|
}
|
||||||
|
tableView.tableFooterView = footerView(canLoadMore: canLoadMore, isLoadingMore: isLoadingMore, isEmpty: isEmpty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func footerLabelFrame(isEmpty: Bool, canLoadMore: Bool) -> UIView? {
|
private func emptyBackgroundView() -> UIView {
|
||||||
guard !canLoadMore || isEmpty else { return nil }
|
let container = UIView(frame: tableView.bounds)
|
||||||
|
emptyLabel.frame = container.bounds
|
||||||
|
emptyLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
container.addSubview(emptyLabel)
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
private func footerView(canLoadMore: Bool, isLoadingMore: Bool, isEmpty: Bool) -> UIView? {
|
||||||
|
guard !isEmpty else { return nil }
|
||||||
|
if isLoadingMore {
|
||||||
|
let container = UIView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: 44))
|
||||||
|
footerSpinner.center = CGPoint(x: container.bounds.midX, y: container.bounds.midY)
|
||||||
|
footerSpinner.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
|
||||||
|
container.addSubview(footerSpinner)
|
||||||
|
footerSpinner.startAnimating()
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
footerSpinner.stopAnimating()
|
||||||
|
guard !canLoadMore else { return nil }
|
||||||
footerLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 44)
|
footerLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 44)
|
||||||
return footerLabel
|
return footerLabel
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
|||||||
listView.apply(
|
listView.apply(
|
||||||
isRefreshing: depositViewModel.isRefreshing,
|
isRefreshing: depositViewModel.isRefreshing,
|
||||||
canLoadMore: depositViewModel.canLoadMore,
|
canLoadMore: depositViewModel.canLoadMore,
|
||||||
isEmpty: depositViewModel.orderList.isEmpty
|
isEmpty: depositViewModel.orderList.isEmpty,
|
||||||
|
isLoading: depositViewModel.isRefreshing && depositViewModel.orderList.isEmpty,
|
||||||
|
isLoadingMore: depositViewModel.isLoadingMore
|
||||||
)
|
)
|
||||||
if let response = depositViewModel.writeOffResponse {
|
if let response = depositViewModel.writeOffResponse {
|
||||||
depositViewModel.dismissWriteOffDialog()
|
depositViewModel.dismissWriteOffDialog()
|
||||||
@ -162,7 +164,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
|||||||
listView.apply(
|
listView.apply(
|
||||||
isRefreshing: orderListViewModel.isRefreshing,
|
isRefreshing: orderListViewModel.isRefreshing,
|
||||||
canLoadMore: orderListViewModel.canLoadMore,
|
canLoadMore: orderListViewModel.canLoadMore,
|
||||||
isEmpty: orderListViewModel.orderList.isEmpty
|
isEmpty: orderListViewModel.orderList.isEmpty,
|
||||||
|
isLoading: orderListViewModel.isRefreshing && orderListViewModel.orderList.isEmpty,
|
||||||
|
isLoadingMore: orderListViewModel.isLoadingMore
|
||||||
)
|
)
|
||||||
if let response = orderListViewModel.signInResponse {
|
if let response = orderListViewModel.signInResponse {
|
||||||
orderListViewModel.clearSignInResponse()
|
orderListViewModel.clearSignInResponse()
|
||||||
@ -186,11 +190,11 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
|||||||
|
|
||||||
private func dateRangeText(start: Date?, end: Date?) -> String {
|
private func dateRangeText(start: Date?, end: Date?) -> String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyy/MM/dd"
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
if let start, let end {
|
if let start, let end {
|
||||||
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
|
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
|
||||||
}
|
}
|
||||||
return "全部"
|
return "时间筛选"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentDateRangeSheet() {
|
private func presentDateRangeSheet() {
|
||||||
@ -427,6 +431,10 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
|||||||
Task { await self?.depositViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
|
Task { await self?.depositViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
actions.onCallPhone = {
|
||||||
|
guard let url = URL(string: "tel://\(item.phone)") else { return }
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
actions.onTap = { [weak self] in
|
actions.onTap = { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
guard self.depositViewModel.canOpenDetail(for: item) else {
|
guard self.depositViewModel.canOpenDetail(for: item) else {
|
||||||
|
|||||||
30
suixinkan/UI/Orders/Views/DepositOrderStatusBadgeView.swift
Normal file
30
suixinkan/UI/Orders/Views/DepositOrderStatusBadgeView.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// DepositOrderStatusBadgeView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 店铺订单状态标签,对齐 Android `DepositOrderCard` badge(12sp)。
|
||||||
|
final class DepositOrderStatusBadgeView: UIView {
|
||||||
|
|
||||||
|
private let badge = AppStatusBadge()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
addSubview(badge)
|
||||||
|
badge.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(status: Int, text: String) {
|
||||||
|
badge.applyDepositOrderStatus(status, text: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
suixinkan/UI/Orders/Views/OrderActionButton.swift
Normal file
49
suixinkan/UI/Orders/Views/OrderActionButton.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//
|
||||||
|
// OrderActionButton.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单卡片操作按钮,对齐 Android `AppButton` 在订单页的样式。
|
||||||
|
final class OrderActionButton: UIButton {
|
||||||
|
|
||||||
|
enum Style {
|
||||||
|
case destructiveLight
|
||||||
|
case primaryFilled
|
||||||
|
case primaryLight
|
||||||
|
case dangerFilled
|
||||||
|
}
|
||||||
|
|
||||||
|
init(title: String, style: Style) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
setTitle(title, for: .normal)
|
||||||
|
titleLabel?.font = .app(.subtitle)
|
||||||
|
layer.cornerRadius = AppRadius.sm
|
||||||
|
clipsToBounds = true
|
||||||
|
switch style {
|
||||||
|
case .destructiveLight:
|
||||||
|
setTitleColor(AppColor.danger, for: .normal)
|
||||||
|
backgroundColor = AppColor.dangerBackground
|
||||||
|
case .primaryFilled:
|
||||||
|
setTitleColor(.white, for: .normal)
|
||||||
|
backgroundColor = AppColor.primary
|
||||||
|
case .primaryLight:
|
||||||
|
setTitleColor(AppColor.primary, for: .normal)
|
||||||
|
backgroundColor = OrderTokens.chipBackground
|
||||||
|
layer.cornerRadius = AppRadius.lg
|
||||||
|
case .dangerFilled:
|
||||||
|
setTitleColor(.white, for: .normal)
|
||||||
|
backgroundColor = AppColor.danger
|
||||||
|
}
|
||||||
|
snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(OrderTokens.actionButtonHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
131
suixinkan/UI/Orders/Views/OrderEditOptionView.swift
Normal file
131
suixinkan/UI/Orders/Views/OrderEditOptionView.swift
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
//
|
||||||
|
// OrderEditOptionView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 「需要剪辑修图」是/否单选,对齐 Android `OrderEditOptionSection`。
|
||||||
|
final class OrderEditOptionView: UIView {
|
||||||
|
|
||||||
|
var onSelectionChange: ((Int) -> Void)?
|
||||||
|
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let readOnlyLabel = UILabel()
|
||||||
|
private let yesButton = UIControl()
|
||||||
|
private let noButton = UIControl()
|
||||||
|
private let yesCircle = UIView()
|
||||||
|
private let noCircle = UIView()
|
||||||
|
private let yesLabel = UILabel()
|
||||||
|
private let noLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
titleLabel.text = "需要剪辑修图"
|
||||||
|
titleLabel.font = .app(.body)
|
||||||
|
titleLabel.textColor = OrderTokens.label563
|
||||||
|
|
||||||
|
readOnlyLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
readOnlyLabel.textColor = OrderTokens.valueBlack
|
||||||
|
readOnlyLabel.isHidden = true
|
||||||
|
|
||||||
|
[yesLabel, noLabel].forEach {
|
||||||
|
$0.text = $0 === yesLabel ? "是" : "否"
|
||||||
|
$0.font = .app(.body)
|
||||||
|
$0.textColor = AppColor.textPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
[yesCircle, noCircle].forEach {
|
||||||
|
$0.layer.cornerRadius = 10
|
||||||
|
$0.layer.borderWidth = 1.5
|
||||||
|
$0.layer.borderColor = AppColor.primary.cgColor
|
||||||
|
$0.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configureChoiceButton(yesButton, circle: yesCircle, label: yesLabel, tag: 1)
|
||||||
|
configureChoiceButton(noButton, circle: noCircle, label: noLabel, tag: 2)
|
||||||
|
|
||||||
|
let choicesStack = UIStackView(arrangedSubviews: [yesButton, noButton])
|
||||||
|
choicesStack.axis = .horizontal
|
||||||
|
choicesStack.spacing = AppSpacing.md
|
||||||
|
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(readOnlyLabel)
|
||||||
|
addSubview(choicesStack)
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
readOnlyLabel.snp.makeConstraints { make in
|
||||||
|
make.trailing.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
choicesStack.snp.makeConstraints { make in
|
||||||
|
make.trailing.centerY.equalToSuperview()
|
||||||
|
}
|
||||||
|
snp.makeConstraints { make in
|
||||||
|
make.height.greaterThanOrEqualTo(28)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(isRefined: Int, readOnly: Bool) {
|
||||||
|
readOnlyLabel.isHidden = !readOnly
|
||||||
|
yesButton.isHidden = readOnly
|
||||||
|
noButton.isHidden = readOnly
|
||||||
|
if readOnly {
|
||||||
|
readOnlyLabel.text = switch isRefined {
|
||||||
|
case 1: "是"
|
||||||
|
case 2: "否"
|
||||||
|
default: "--"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateCircle(yesCircle, selected: isRefined == 1)
|
||||||
|
updateCircle(noCircle, selected: isRefined == 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureChoiceButton(_ control: UIControl, circle: UIView, label: UILabel, tag: Int) {
|
||||||
|
control.tag = tag
|
||||||
|
control.addTarget(self, action: #selector(choiceTapped(_:)), for: .touchUpInside)
|
||||||
|
let row = UIStackView(arrangedSubviews: [circle, label])
|
||||||
|
row.axis = .horizontal
|
||||||
|
row.spacing = AppSpacing.xxs
|
||||||
|
row.isUserInteractionEnabled = false
|
||||||
|
control.addSubview(row)
|
||||||
|
row.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCircle(_ circle: UIView, selected: Bool) {
|
||||||
|
if selected {
|
||||||
|
circle.backgroundColor = AppColor.primary
|
||||||
|
circle.layer.borderWidth = 0
|
||||||
|
let check = UIImageView(image: UIImage(systemName: "checkmark"))
|
||||||
|
check.tintColor = .white
|
||||||
|
check.tag = 999
|
||||||
|
circle.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
circle.addSubview(check)
|
||||||
|
check.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
make.width.height.equalTo(12)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
circle.backgroundColor = .clear
|
||||||
|
circle.layer.borderWidth = 1.5
|
||||||
|
circle.layer.borderColor = AppColor.primary.cgColor
|
||||||
|
circle.subviews.forEach { $0.removeFromSuperview() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func choiceTapped(_ sender: UIControl) {
|
||||||
|
onSelectionChange?(sender.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
suixinkan/UI/Orders/Views/OrderFilterPillControl.swift
Normal file
66
suixinkan/UI/Orders/Views/OrderFilterPillControl.swift
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// OrderFilterPillControl.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单筛选灰 pill,对齐 Android 筛选/时间控件。
|
||||||
|
final class OrderFilterPillControl: UIButton {
|
||||||
|
|
||||||
|
private let pillTitleLabel = UILabel()
|
||||||
|
private let trailingIconView = UIImageView()
|
||||||
|
|
||||||
|
var pillTitle: String = "" {
|
||||||
|
didSet { pillTitleLabel.text = pillTitle }
|
||||||
|
}
|
||||||
|
|
||||||
|
var showsTrailingIcon: Bool = false {
|
||||||
|
didSet { trailingIconView.isHidden = !showsTrailingIcon }
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = OrderTokens.filterBackground
|
||||||
|
layer.cornerRadius = OrderTokens.pillRadius
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
pillTitleLabel.font = .app(.body)
|
||||||
|
pillTitleLabel.textColor = OrderTokens.label563
|
||||||
|
pillTitleLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
pillTitleLabel.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
trailingIconView.contentMode = .scaleAspectFit
|
||||||
|
trailingIconView.tintColor = OrderTokens.label563
|
||||||
|
trailingIconView.isHidden = true
|
||||||
|
trailingIconView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
addSubview(pillTitleLabel)
|
||||||
|
addSubview(trailingIconView)
|
||||||
|
|
||||||
|
pillTitleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.equalToSuperview().inset(AppSpacing.sm)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.trailing.lessThanOrEqualTo(trailingIconView.snp.leading).offset(-AppSpacing.xxs)
|
||||||
|
}
|
||||||
|
trailingIconView.snp.makeConstraints { make in
|
||||||
|
make.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(15)
|
||||||
|
}
|
||||||
|
snp.makeConstraints { make in
|
||||||
|
make.height.equalTo(OrderTokens.filterPillHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTrailingSystemImage(_ name: String) {
|
||||||
|
trailingIconView.image = UIImage(systemName: name)
|
||||||
|
showsTrailingIcon = true
|
||||||
|
}
|
||||||
|
}
|
||||||
61
suixinkan/UI/Orders/Views/OrderGiftStepperView.swift
Normal file
61
suixinkan/UI/Orders/Views/OrderGiftStepperView.swift
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// OrderGiftStepperView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 修图/视频赠送步进控件,+/- 均触发赠送弹窗(对齐 Android)。
|
||||||
|
final class OrderGiftStepperView: UIView {
|
||||||
|
|
||||||
|
var onMinusTap: (() -> Void)?
|
||||||
|
var onPlusTap: (() -> Void)?
|
||||||
|
|
||||||
|
private let minusButton = UIButton(type: .system)
|
||||||
|
private let plusButton = UIButton(type: .system)
|
||||||
|
private let valueLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
[minusButton, plusButton].forEach { button in
|
||||||
|
button.layer.cornerRadius = 12
|
||||||
|
button.layer.borderWidth = 1
|
||||||
|
button.layer.borderColor = OrderTokens.giftBorder.cgColor
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
|
button.setTitleColor(OrderTokens.giftBorder, for: .normal)
|
||||||
|
button.snp.makeConstraints { make in
|
||||||
|
make.width.height.equalTo(24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
minusButton.setTitle("-", for: .normal)
|
||||||
|
plusButton.setTitle("+", for: .normal)
|
||||||
|
minusButton.addTarget(self, action: #selector(minusTapped), for: .touchUpInside)
|
||||||
|
plusButton.addTarget(self, action: #selector(plusTapped), for: .touchUpInside)
|
||||||
|
|
||||||
|
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
valueLabel.textColor = OrderTokens.valueBlack
|
||||||
|
valueLabel.textAlignment = .center
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [minusButton, valueLabel, plusButton])
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = AppSpacing.xs
|
||||||
|
stack.alignment = .center
|
||||||
|
addSubview(stack)
|
||||||
|
stack.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(text: String) {
|
||||||
|
valueLabel.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func minusTapped() { onMinusTap?() }
|
||||||
|
@objc private func plusTapped() { onPlusTap?() }
|
||||||
|
}
|
||||||
109
suixinkan/UI/Orders/Views/OrderHistoricalPhotosView.swift
Normal file
109
suixinkan/UI/Orders/Views/OrderHistoricalPhotosView.swift
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// OrderHistoricalPhotosView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 历史拍摄缩略图行,对齐 Android multi_travel 区块。
|
||||||
|
final class OrderHistoricalPhotosView: UIView {
|
||||||
|
|
||||||
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let divider = UIView()
|
||||||
|
private let photosStack = UIStackView()
|
||||||
|
private var imageViews: [UIImageView] = []
|
||||||
|
private var overlayLabel: UILabel?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
divider.backgroundColor = OrderTokens.remarkDivider
|
||||||
|
|
||||||
|
titleLabel.text = "历史拍摄信息:"
|
||||||
|
titleLabel.font = .app(.body)
|
||||||
|
titleLabel.textColor = OrderTokens.label563
|
||||||
|
|
||||||
|
photosStack.axis = .horizontal
|
||||||
|
photosStack.spacing = AppSpacing.xs
|
||||||
|
photosStack.distribution = .fillEqually
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||||
|
addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
addSubview(divider)
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(photosStack)
|
||||||
|
|
||||||
|
divider.snp.makeConstraints { make in
|
||||||
|
make.top.leading.trailing.equalToSuperview()
|
||||||
|
make.height.equalTo(1)
|
||||||
|
}
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(divider.snp.bottom).offset(AppSpacing.sm)
|
||||||
|
make.leading.trailing.equalToSuperview()
|
||||||
|
}
|
||||||
|
photosStack.snp.makeConstraints { make in
|
||||||
|
make.top.equalTo(titleLabel.snp.bottom).offset(AppSpacing.xs)
|
||||||
|
make.leading.trailing.bottom.equalToSuperview()
|
||||||
|
make.height.equalTo(72)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(materials: [MultiTravelMaterialEntity]) {
|
||||||
|
isHidden = materials.isEmpty
|
||||||
|
photosStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
imageViews.removeAll()
|
||||||
|
overlayLabel?.removeFromSuperview()
|
||||||
|
overlayLabel = nil
|
||||||
|
|
||||||
|
let displayItems = Array(materials.prefix(3))
|
||||||
|
for (index, material) in displayItems.enumerated() {
|
||||||
|
let container = UIView()
|
||||||
|
container.backgroundColor = OrderTokens.filterBackground
|
||||||
|
container.layer.cornerRadius = AppRadius.sm
|
||||||
|
container.clipsToBounds = true
|
||||||
|
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
let thumbURL = material.coverUrl.isEmpty ? material.fileUrl : material.coverUrl
|
||||||
|
imageView.loadRemoteImage(urlString: thumbURL, placeholderColor: OrderTokens.filterBackground)
|
||||||
|
container.addSubview(imageView)
|
||||||
|
imageView.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
imageViews.append(imageView)
|
||||||
|
photosStack.addArrangedSubview(container)
|
||||||
|
|
||||||
|
if materials.count > 3, index == 2 {
|
||||||
|
let overlay = UIView()
|
||||||
|
overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "查看更多"
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = .white
|
||||||
|
label.textAlignment = .center
|
||||||
|
overlay.addSubview(label)
|
||||||
|
label.snp.makeConstraints { make in
|
||||||
|
make.center.equalToSuperview()
|
||||||
|
}
|
||||||
|
container.addSubview(overlay)
|
||||||
|
overlay.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview()
|
||||||
|
}
|
||||||
|
overlayLabel = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleTap() {
|
||||||
|
onTap?()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
suixinkan/UI/Orders/Views/OrderInfoRowView.swift
Normal file
46
suixinkan/UI/Orders/Views/OrderInfoRowView.swift
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//
|
||||||
|
// OrderInfoRowView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单详情 key-value 行,对齐 Android InfoRow。
|
||||||
|
final class OrderInfoRowView: UIView {
|
||||||
|
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let valueLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
titleLabel.font = .app(.body)
|
||||||
|
titleLabel.textColor = OrderTokens.label563
|
||||||
|
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
valueLabel.textColor = OrderTokens.valueBlack
|
||||||
|
valueLabel.textAlignment = .right
|
||||||
|
valueLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(valueLabel)
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.top.bottom.equalToSuperview()
|
||||||
|
make.width.lessThanOrEqualToSuperview().multipliedBy(0.45)
|
||||||
|
}
|
||||||
|
valueLabel.snp.makeConstraints { make in
|
||||||
|
make.trailing.top.bottom.equalToSuperview()
|
||||||
|
make.leading.greaterThanOrEqualTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(title: String, value: String, valueColor: UIColor = OrderTokens.valueBlack, usesPrimaryValue: Bool = false) {
|
||||||
|
titleLabel.text = "\(title):"
|
||||||
|
valueLabel.text = value
|
||||||
|
valueLabel.textColor = usesPrimaryValue ? AppColor.primary : valueColor
|
||||||
|
}
|
||||||
|
}
|
||||||
101
suixinkan/UI/Orders/Views/OrderPhoneRowView.swift
Normal file
101
suixinkan/UI/Orders/Views/OrderPhoneRowView.swift
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// OrderPhoneRowView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单手机号行,支持摄影师/店铺两种 Android 样式。
|
||||||
|
final class OrderPhoneRowView: UIView {
|
||||||
|
|
||||||
|
enum Style {
|
||||||
|
case photographer
|
||||||
|
case deposit
|
||||||
|
}
|
||||||
|
|
||||||
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
private let style: Style
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let phoneLabel = UILabel()
|
||||||
|
private let phoneButton = UIButton(type: .system)
|
||||||
|
private let iconButton = UIButton(type: .system)
|
||||||
|
|
||||||
|
init(style: Style) {
|
||||||
|
self.style = style
|
||||||
|
super.init(frame: .zero)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(phone: String, hidden: Bool = false) {
|
||||||
|
let masked = OrderPhoneMask.mask(phone)
|
||||||
|
phoneLabel.text = masked
|
||||||
|
isHidden = hidden || phone.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
titleLabel.font = .app(.body)
|
||||||
|
titleLabel.textColor = OrderTokens.label563
|
||||||
|
titleLabel.text = "手机号:"
|
||||||
|
|
||||||
|
phoneLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
phoneLabel.textAlignment = .right
|
||||||
|
|
||||||
|
phoneButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
|
||||||
|
iconButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
|
||||||
|
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(phoneLabel)
|
||||||
|
addSubview(phoneButton)
|
||||||
|
addSubview(iconButton)
|
||||||
|
|
||||||
|
titleLabel.snp.makeConstraints { make in
|
||||||
|
make.leading.top.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch style {
|
||||||
|
case .photographer:
|
||||||
|
phoneLabel.textColor = OrderTokens.valueBlack
|
||||||
|
iconButton.backgroundColor = OrderTokens.phoneIconBackground
|
||||||
|
iconButton.layer.cornerRadius = 12
|
||||||
|
iconButton.tintColor = AppColor.primary
|
||||||
|
iconButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
||||||
|
iconButton.snp.makeConstraints { make in
|
||||||
|
make.trailing.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(24)
|
||||||
|
}
|
||||||
|
phoneLabel.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.trailing.equalTo(iconButton.snp.leading).offset(-7)
|
||||||
|
}
|
||||||
|
case .deposit:
|
||||||
|
phoneLabel.textColor = AppColor.primary
|
||||||
|
iconButton.tintColor = AppColor.primary
|
||||||
|
iconButton.setImage(UIImage(systemName: "phone.fill"), for: .normal)
|
||||||
|
iconButton.snp.makeConstraints { make in
|
||||||
|
make.trailing.centerY.equalToSuperview()
|
||||||
|
make.width.height.equalTo(16)
|
||||||
|
}
|
||||||
|
phoneLabel.snp.makeConstraints { make in
|
||||||
|
make.centerY.equalToSuperview()
|
||||||
|
make.trailing.equalTo(iconButton.snp.leading).offset(-AppSpacing.xxs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
phoneButton.snp.makeConstraints { make in
|
||||||
|
make.leading.equalTo(phoneLabel)
|
||||||
|
make.trailing.equalTo(iconButton)
|
||||||
|
make.top.bottom.equalToSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleTap() {
|
||||||
|
onTap?()
|
||||||
|
}
|
||||||
|
}
|
||||||
37
suixinkan/UI/Orders/Views/OrderTypeChipView.swift
Normal file
37
suixinkan/UI/Orders/Views/OrderTypeChipView.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//
|
||||||
|
// OrderTypeChipView.swift
|
||||||
|
// suixinkan
|
||||||
|
//
|
||||||
|
|
||||||
|
import SnapKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// 订单类型标签 chip。
|
||||||
|
final class OrderTypeChipView: UIView {
|
||||||
|
|
||||||
|
private let label = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = OrderTokens.chipBackground
|
||||||
|
layer.cornerRadius = OrderTokens.pillRadius
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
label.font = .app(.caption)
|
||||||
|
label.textColor = AppColor.primary
|
||||||
|
addSubview(label)
|
||||||
|
label.snp.makeConstraints { make in
|
||||||
|
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: AppSpacing.xxs, bottom: 0, right: AppSpacing.xxs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(text: String) {
|
||||||
|
label.text = text
|
||||||
|
isHidden = text.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
64
suixinkanTests/OrderListUITokensTests.swift
Normal file
64
suixinkanTests/OrderListUITokensTests.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// OrderListUITokensTests.swift
|
||||||
|
// suixinkanTests
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
@testable import suixinkan
|
||||||
|
|
||||||
|
/// 订单列表 UI Token 与 badge 映射校验。
|
||||||
|
final class OrderListUITokensTests: XCTestCase {
|
||||||
|
|
||||||
|
func testOrderTokensMatchAndroid() {
|
||||||
|
XCTAssertEqual(OrderTokens.filterBackground.hexRGB, 0xF4F4F4)
|
||||||
|
XCTAssertEqual(OrderTokens.label563.hexRGB, 0x4B5563)
|
||||||
|
XCTAssertEqual(OrderTokens.timestamp.hexRGB, 0x6B7280)
|
||||||
|
XCTAssertEqual(OrderTokens.depositSuccess.hexRGB, 0x27AE60)
|
||||||
|
XCTAssertEqual(OrderTokens.depositRefunded.hexRGB, 0x6B7280)
|
||||||
|
XCTAssertEqual(OrderTokens.searchHeight, 36)
|
||||||
|
XCTAssertEqual(OrderTokens.filterPillHeight, 35)
|
||||||
|
XCTAssertEqual(OrderTokens.actionButtonHeight, 48)
|
||||||
|
XCTAssertEqual(OrderTokens.cardRadius, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPhotographerOrderStatusBadgeMapping() {
|
||||||
|
let badge = AppStatusBadge()
|
||||||
|
badge.applyOrderStatus(18, text: "已付定金")
|
||||||
|
badge.applyOrderStatus(30, text: "已完成")
|
||||||
|
badge.applyOrderStatus(50, text: "已退款")
|
||||||
|
XCTAssertFalse(badge.isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDepositOrderStatus50UsesNeutralGray() {
|
||||||
|
let badge = AppStatusBadge()
|
||||||
|
badge.applyDepositOrderStatus(50, text: "已退款")
|
||||||
|
XCTAssertEqual(badge.backgroundColor?.hexRGB, OrderTokens.depositRefundedBackground.hexRGB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDepositOrderStatus30UsesDepositGreen() {
|
||||||
|
let badge = AppStatusBadge()
|
||||||
|
badge.applyDepositOrderStatus(30, text: "已完成")
|
||||||
|
XCTAssertEqual(badge.backgroundColor?.hexRGB, OrderTokens.depositSuccessBackground.hexRGB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPhotographerStatus50UsesDangerNotGray() {
|
||||||
|
let badge = AppStatusBadge()
|
||||||
|
badge.applyOrderStatus(50, text: "已退款")
|
||||||
|
XCTAssertEqual(badge.backgroundColor?.hexRGB, AppColor.dangerBackground.hexRGB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UIColor {
|
||||||
|
var hexRGB: UInt {
|
||||||
|
var red: CGFloat = 0
|
||||||
|
var green: CGFloat = 0
|
||||||
|
var blue: CGFloat = 0
|
||||||
|
var alpha: CGFloat = 0
|
||||||
|
getRed(&red, green: &green, blue: &blue, alpha: &alpha)
|
||||||
|
let r = Int(round(red * 255))
|
||||||
|
let g = Int(round(green * 255))
|
||||||
|
let b = Int(round(blue * 255))
|
||||||
|
return UInt(r << 16 | g << 8 | b)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user