Rebuild order list UI to strictly match Android OrderView and DepositOrderCard.
Add OrderTokens and shared order components, restore filter bar pills, list loading states, and role-specific card layouts with deposit badge mapping tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -10,29 +10,35 @@ struct DepositOrderCardActions {
|
||||
var onRefund: (() -> Void)?
|
||||
var onPartialRefund: (() -> Void)?
|
||||
var onVerify: (() -> Void)?
|
||||
var onCallPhone: (() -> Void)?
|
||||
var onTap: (() -> Void)?
|
||||
}
|
||||
|
||||
/// 店铺订单卡片,对齐 Android `DepositOrderListScreen`。
|
||||
/// 店铺订单卡片,严格对齐 Android `DepositOrderCard`。
|
||||
final class DepositOrderCardCell: UITableViewCell {
|
||||
static let reuseIdentifier = "DepositOrderCardCell"
|
||||
|
||||
private let cardView = UIView()
|
||||
private let projectLabel = UILabel()
|
||||
private let statusBadge = OrderStatusBadgeView()
|
||||
private let amountLabel = UILabel()
|
||||
private let timeLabel = UILabel()
|
||||
private let uidLabel = UILabel()
|
||||
private let phoneLabel = UILabel()
|
||||
private let refundButton = UIButton(type: .system)
|
||||
private let partialRefundButton = UIButton(type: .system)
|
||||
private let verifyButton = UIButton(type: .system)
|
||||
private let contentStack = UIStackView()
|
||||
private let headerRow = UIStackView()
|
||||
private let orderNumberLabel = UILabel()
|
||||
private let statusBadge = DepositOrderStatusBadgeView()
|
||||
private let createdAtLabel = UILabel()
|
||||
private let typeChip = OrderTypeChipView()
|
||||
private let infoStack = UIStackView()
|
||||
private let phoneRow = OrderPhoneRowView(style: .deposit)
|
||||
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()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@ -43,98 +49,76 @@ final class DepositOrderCardCell: UITableViewCell {
|
||||
|
||||
func configure(item: StoreOrderItem, actions: DepositOrderCardActions) {
|
||||
self.actions = actions
|
||||
projectLabel.text = item.projectName.isEmpty ? item.orderTypeLabel : item.projectName
|
||||
orderNumberLabel.text = "订单号: \(item.orderNumber)"
|
||||
statusBadge.apply(status: item.orderStatus, text: item.orderStatusName)
|
||||
amountLabel.text = "¥\(item.actualPayAmount)"
|
||||
timeLabel.text = item.createdAt
|
||||
uidLabel.text = "UID:\(item.userId)"
|
||||
phoneLabel.text = OrderPhoneMask.mask(item.phone)
|
||||
createdAtLabel.text = item.createdAt
|
||||
typeChip.apply(text: item.orderTypeLabel)
|
||||
|
||||
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
|
||||
verifyButton.isHidden = !showVerify
|
||||
refundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
|
||||
partialRefundButton.isHidden = item.orderStatus != 20 && item.orderStatus != 30
|
||||
}
|
||||
|
||||
private func setupUI() {
|
||||
cardView.backgroundColor = .white
|
||||
cardView.layer.cornerRadius = 12
|
||||
cardView.backgroundColor = AppColor.cardBackground
|
||||
cardView.layer.cornerRadius = OrderTokens.cardRadius
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
|
||||
cardView.addGestureRecognizer(tap)
|
||||
|
||||
projectLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
amountLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||
amountLabel.textColor = AppColor.primary
|
||||
[timeLabel, uidLabel, phoneLabel].forEach {
|
||||
$0.font = .systemFont(ofSize: 13)
|
||||
$0.textColor = UIColor(hex: 0x6B7280)
|
||||
}
|
||||
orderNumberLabel.font = .app(.body)
|
||||
orderNumberLabel.textColor = OrderTokens.orderNumber
|
||||
createdAtLabel.font = .app(.caption)
|
||||
createdAtLabel.textColor = OrderTokens.timestamp
|
||||
|
||||
configureActionButton(refundButton, title: "退款", color: UIColor(hex: 0xEF4444), bg: UIColor(hex: 0xFFE7E7))
|
||||
configureActionButton(partialRefundButton, title: "部分退款", color: AppColor.primary, bg: UIColor(hex: 0xEFF6FF))
|
||||
configureActionButton(verifyButton, title: "线下核销", color: .white, bg: UIColor(hex: 0x0073FF))
|
||||
headerRow.axis = .horizontal
|
||||
headerRow.alignment = .center
|
||||
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)
|
||||
partialRefundButton.addTarget(self, action: #selector(partialRefundTapped), for: .touchUpInside)
|
||||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||||
|
||||
contentView.addSubview(cardView)
|
||||
[projectLabel, statusBadge, amountLabel, timeLabel, uidLabel, phoneLabel, refundButton, partialRefundButton, verifyButton].forEach {
|
||||
cardView.addSubview($0)
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = OrderTokens.cardGap
|
||||
[headerRow, createdAtLabel, typeChip, infoStack, actionRow, verifyButton].forEach {
|
||||
contentStack.addArrangedSubview($0)
|
||||
}
|
||||
|
||||
contentView.addSubview(cardView)
|
||||
cardView.addSubview(contentStack)
|
||||
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
|
||||
make.top.leading.equalToSuperview().inset(16)
|
||||
}
|
||||
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)
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(color, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
button.backgroundColor = bg
|
||||
button.layer.cornerRadius = 8
|
||||
private func addInfoRow(title: String, value: String, primary: Bool = false) {
|
||||
let row = OrderInfoRowView()
|
||||
row.apply(title: title, value: value, usesPrimaryValue: primary)
|
||||
infoStack.addArrangedSubview(row)
|
||||
}
|
||||
|
||||
@objc private func refundTapped() { actions.onRefund?() }
|
||||
|
||||
@ -22,27 +22,43 @@ struct OrderCardActions {
|
||||
var onOrderSourceTap: (() -> Void)?
|
||||
}
|
||||
|
||||
/// 摄影师订单卡片,对齐 Android `OrderView`。
|
||||
/// 摄影师订单卡片,严格对齐 Android `OrderView`。
|
||||
final class OrderCardCell: UITableViewCell {
|
||||
static let reuseIdentifier = "OrderCardCell"
|
||||
|
||||
private let cardView = UIView()
|
||||
private let contentStack = UIStackView()
|
||||
|
||||
private let headerRow = UIStackView()
|
||||
private let orderNumberLabel = UILabel()
|
||||
private let statusBadge = OrderStatusBadgeView()
|
||||
private let createdAtLabel = UILabel()
|
||||
private let typeChip = UILabel()
|
||||
private let actionStack = UIStackView()
|
||||
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 typeChip = OrderTypeChipView()
|
||||
private let orderSourceContainer = UIView()
|
||||
private let orderSourceButton = UIButton(type: .system)
|
||||
private let historicalButton = UIButton(type: .system)
|
||||
private let refinedStack = UIStackView()
|
||||
private let refinedNeedButton = UIButton(type: .system)
|
||||
private let refinedNoButton = UIButton(type: .system)
|
||||
private let actionRow = UIStackView()
|
||||
private let refundButton = OrderActionButton(title: "退款", style: .destructiveLight)
|
||||
private let verifyButton = OrderActionButton(title: "核销订单", style: .primaryFilled)
|
||||
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()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
@ -58,249 +74,266 @@ final class OrderCardCell: UITableViewCell {
|
||||
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
|
||||
orderNumberLabel.text = "订单号:\(order.orderNumber)"
|
||||
statusBadge.apply(status: order.orderStatus, text: order.orderStatusName)
|
||||
createdAtLabel.text = order.createdAt.isEmpty ? "--" : order.createdAt
|
||||
typeChip.text = order.orderTypeLabel
|
||||
typeChip.isHidden = order.orderTypeLabel.isEmpty
|
||||
typeChip.apply(text: order.orderTypeLabel)
|
||||
|
||||
let showRefund = !hideWriteActions && (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
|
||||
let showVerify = !hideWriteActions && (
|
||||
(order.photoTravel?.needCheckIn == true && order.orderStatus != 30) || order.orderType == 19
|
||||
)
|
||||
actionRow.isHidden = !showRefund && !showVerify
|
||||
refundButton.isHidden = !showRefund
|
||||
verifyButton.isHidden = !showVerify
|
||||
actionStack.isHidden = !showRefund && !showVerify
|
||||
updateRefundWidth(showVerify: showVerify && showRefund)
|
||||
|
||||
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
|
||||
|
||||
orderSourceButton.isHidden = !showOrderSource
|
||||
orderSourceContainer.isHidden = !showOrderSource
|
||||
orderSourceButton.setTitle(sourceText ?? "选择订单来源", for: .normal)
|
||||
|
||||
let showRefined = order.orderStatus == 18 || order.orderStatus == 30
|
||||
refinedStack.isHidden = !showRefined
|
||||
updateRefinedButtons(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)
|
||||
let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19
|
||||
editOptionView.isHidden = !showEdit
|
||||
if showEdit {
|
||||
editOptionView.apply(isRefined: order.isRefined, readOnly: hideWriteActions)
|
||||
}
|
||||
if let travel = order.photoTravel {
|
||||
addInfoRow(title: "修图张数", value: "\(travel.orderPhotoNum)")
|
||||
addInfoRow(title: "视频", value: "\(travel.orderVideoNum)")
|
||||
|
||||
let showRefundAmount = order.orderStatus == 50
|
||||
&& !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 {
|
||||
addInfoRow(title: "备注", value: remark)
|
||||
remarkSection.isHidden = false
|
||||
remarkBodyLabel.text = remark
|
||||
} else {
|
||||
remarkSection.isHidden = true
|
||||
}
|
||||
|
||||
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() {
|
||||
cardView.backgroundColor = AppColor.cardBackground
|
||||
cardView.layer.cornerRadius = AppRadius.lg
|
||||
cardView.layer.cornerRadius = OrderTokens.cardRadius
|
||||
|
||||
orderNumberLabel.font = .app(.body)
|
||||
orderNumberLabel.textColor = AppColor.textPrimary
|
||||
orderNumberLabel.textColor = OrderTokens.orderNumber
|
||||
createdAtLabel.font = .app(.caption)
|
||||
createdAtLabel.textColor = AppColor.textSecondary
|
||||
typeChip.font = .app(.caption)
|
||||
typeChip.textColor = AppColor.primary
|
||||
typeChip.backgroundColor = AppColor.primaryLight
|
||||
typeChip.layer.cornerRadius = AppRadius.xs
|
||||
typeChip.clipsToBounds = true
|
||||
typeChip.textAlignment = .center
|
||||
createdAtLabel.textColor = OrderTokens.timestamp
|
||||
|
||||
configureActionButton(refundButton, title: "退款", color: AppColor.danger, bg: AppColor.dangerBackground)
|
||||
configureActionButton(verifyButton, title: "核销订单", color: .white, bg: AppColor.primary)
|
||||
refundButton.addTarget(self, action: #selector(refundTapped), for: .touchUpInside)
|
||||
verifyButton.addTarget(self, action: #selector(verifyTapped), for: .touchUpInside)
|
||||
|
||||
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)
|
||||
headerRow.axis = .horizontal
|
||||
headerRow.alignment = .center
|
||||
headerRow.distribution = .equalSpacing
|
||||
headerRow.addArrangedSubview(orderNumberLabel)
|
||||
headerRow.addArrangedSubview(statusBadge)
|
||||
|
||||
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.titleLabel?.font = .app(.body)
|
||||
orderSourceButton.contentHorizontalAlignment = .leading
|
||||
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
|
||||
|
||||
historicalButton.setTitle("历史拍摄", for: .normal)
|
||||
historicalButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
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
|
||||
orderSourceContainer.addSubview(orderSourceButton)
|
||||
orderSourceButton.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(AppSpacing.sm)
|
||||
}
|
||||
refinedNeedButton.addTarget(self, action: #selector(refinedNeedTapped), for: .touchUpInside)
|
||||
refinedNoButton.addTarget(self, action: #selector(refinedNoTapped), for: .touchUpInside)
|
||||
refinedStack.addArrangedSubview(refinedNeedButton)
|
||||
refinedStack.addArrangedSubview(refinedNoButton)
|
||||
|
||||
actionRow.axis = .horizontal
|
||||
actionRow.spacing = OrderTokens.cardGap
|
||||
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)
|
||||
cardView.addSubview(orderNumberLabel)
|
||||
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.addSubview(contentStack)
|
||||
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
|
||||
make.top.leading.equalToSuperview().inset(16)
|
||||
}
|
||||
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)
|
||||
contentStack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(OrderTokens.cardPadding)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureActionButton(_ button: UIButton, title: String, color: UIColor, bg: UIColor) {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.setTitleColor(color, for: .normal)
|
||||
button.titleLabel?.font = .app(.subtitle)
|
||||
button.backgroundColor = bg
|
||||
button.layer.cornerRadius = AppRadius.sm
|
||||
}
|
||||
|
||||
private func addInfoRow(title: String, value: String) {
|
||||
let row = UIStackView()
|
||||
private func configureTravelRow(
|
||||
_ row: UIStackView,
|
||||
titleLabel: UILabel,
|
||||
valueLabel: UILabel,
|
||||
title: String,
|
||||
stepper: OrderGiftStepperView
|
||||
) {
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
let titleLabel = UILabel()
|
||||
row.alignment = .center
|
||||
row.distribution = .equalSpacing
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .app(.body)
|
||||
titleLabel.textColor = AppColor.textSecondary
|
||||
let valueLabel = UILabel()
|
||||
valueLabel.text = value
|
||||
valueLabel.font = .app(.body)
|
||||
valueLabel.textColor = AppColor.textPrimary
|
||||
valueLabel.textAlignment = .right
|
||||
titleLabel.textColor = OrderTokens.label563
|
||||
valueLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
valueLabel.textColor = OrderTokens.valueBlack
|
||||
row.addArrangedSubview(titleLabel)
|
||||
row.addArrangedSubview(stepper)
|
||||
row.addArrangedSubview(valueLabel)
|
||||
infoStack.addArrangedSubview(row)
|
||||
valueLabel.isHidden = true
|
||||
}
|
||||
|
||||
private func addPhoneRow(phone: String, hidden: Bool) {
|
||||
let row = UIStackView()
|
||||
row.axis = .horizontal
|
||||
row.distribution = .fillEqually
|
||||
let titleLabel = UILabel()
|
||||
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 updateRefundWidth(showVerify: Bool) {
|
||||
refundWidthConstraint?.deactivate()
|
||||
if showVerify {
|
||||
refundButton.snp.makeConstraints { make in
|
||||
refundWidthConstraint = make.width.equalTo(actionRow.snp.width).multipliedBy(OrderTokens.refundButtonWidthRatio).constraint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRefinedButtons(isRefined: Int, readOnly: Bool) {
|
||||
let selectedBorder = AppColor.primary.cgColor
|
||||
let normalBorder = AppColor.border.cgColor
|
||||
refinedNeedButton.layer.borderColor = isRefined == 1 ? selectedBorder : normalBorder
|
||||
refinedNoButton.layer.borderColor = isRefined == 2 ? selectedBorder : normalBorder
|
||||
refinedNeedButton.isEnabled = !readOnly
|
||||
refinedNoButton.isEnabled = !readOnly
|
||||
private func rebuildDetailRows(order: OrderEntity, hideWriteActions: Bool) {
|
||||
detailStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
addDetailRow(title: "付款金额", value: "¥\(order.actualPayAmount)", primary: true)
|
||||
addDetailRow(title: "付款时间", value: resolvedPayTime(order))
|
||||
addDetailRow(title: "完成时间", value: order.completeTime.isEmpty ? "--" : order.completeTime)
|
||||
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 verifyTapped() { actions.onVerify?() }
|
||||
@objc private func uploadTaskTapped() { actions.onUploadTask?() }
|
||||
@objc private func cancelTapped() { actions.onCancel?() }
|
||||
@objc private func callTapped() { actions.onCallPhone?() }
|
||||
@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 {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 订单筛选栏:搜索 + 状态下拉 + 日期筛选。
|
||||
/// 订单筛选栏:搜索 + 状态下拉 + 日期筛选,对齐 Android Header。
|
||||
final class OrderFilterBarView: UIView {
|
||||
|
||||
var onSearchChanged: ((String) -> Void)?
|
||||
@ -14,16 +14,18 @@ final class OrderFilterBarView: UIView {
|
||||
var onFilterSelected: ((Int, Int) -> Void)?
|
||||
var onDateRangeTap: (() -> Void)?
|
||||
|
||||
private let searchContainer = UIView()
|
||||
private let searchIconView = UIImageView(image: UIImage(systemName: "magnifyingglass"))
|
||||
private let searchField = UITextField()
|
||||
private let filterButton = UIButton(type: .system)
|
||||
private let dateButton = UIButton(type: .system)
|
||||
private let divider = UIView()
|
||||
private let filterPill = OrderFilterPillControl()
|
||||
private let datePill = OrderFilterPillControl()
|
||||
|
||||
private var isStoreMode = false
|
||||
private var currentFilterLabel = "全部"
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
backgroundColor = AppColor.cardBackground
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@ -34,95 +36,102 @@ final class OrderFilterBarView: UIView {
|
||||
|
||||
func configure(isStoreMode: Bool, filterLabel: String, dateRangeText: String, searchText: String) {
|
||||
self.isStoreMode = isStoreMode
|
||||
currentFilterLabel = filterLabel
|
||||
searchField.placeholder = isStoreMode ? "输入游客手机号" : "输入手机号搜索"
|
||||
searchField.attributedPlaceholder = NSAttributedString(
|
||||
string: searchField.placeholder ?? "",
|
||||
attributes: [.foregroundColor: OrderTokens.searchPlaceholder]
|
||||
)
|
||||
searchField.text = searchText
|
||||
filterButton.setTitle("\(filterLabel) ▾", for: .normal)
|
||||
dateButton.setTitle(dateRangeText, for: .normal)
|
||||
filterPill.pillTitle = filterLabel
|
||||
datePill.pillTitle = dateRangeText
|
||||
rebuildFilterMenu(selectedLabel: filterLabel)
|
||||
}
|
||||
|
||||
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.returnKeyType = .search
|
||||
searchField.backgroundColor = UIColor(hex: 0xF4F4F4)
|
||||
searchField.layer.cornerRadius = 4
|
||||
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.borderStyle = .none
|
||||
searchField.backgroundColor = .clear
|
||||
searchField.delegate = self
|
||||
searchField.addTarget(self, action: #selector(searchChanged), for: .editingChanged)
|
||||
|
||||
filterButton.setTitleColor(AppColor.text333, for: .normal)
|
||||
filterButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
|
||||
divider.backgroundColor = AppColor.border
|
||||
|
||||
dateButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
dateButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
dateButton.contentHorizontalAlignment = .trailing
|
||||
dateButton.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
|
||||
filterPill.showsMenuAsPrimaryAction = true
|
||||
|
||||
addSubview(searchField)
|
||||
addSubview(filterButton)
|
||||
addSubview(dateButton)
|
||||
datePill.setTrailingSystemImage("calendar")
|
||||
datePill.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
|
||||
|
||||
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
|
||||
make.top.leading.trailing.equalToSuperview().inset(16)
|
||||
make.height.equalTo(36)
|
||||
make.leading.equalTo(searchIconView.snp.trailing).offset(10)
|
||||
make.trailing.equalToSuperview().inset(AppSpacing.sm)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
filterButton.snp.makeConstraints { make in
|
||||
make.top.equalTo(searchField.snp.bottom).offset(12)
|
||||
make.leading.equalToSuperview().inset(16)
|
||||
make.bottom.equalToSuperview().inset(12)
|
||||
divider.snp.makeConstraints { make in
|
||||
make.top.equalTo(searchContainer.snp.bottom).offset(AppSpacing.md)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(OrderTokens.dividerHeight)
|
||||
}
|
||||
dateButton.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(filterButton)
|
||||
make.trailing.equalToSuperview().inset(16)
|
||||
filterPill.snp.makeConstraints { make in
|
||||
make.top.equalTo(divider.snp.bottom).offset(AppSpacing.md)
|
||||
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() {
|
||||
onSearchChanged?(searchField.text ?? "")
|
||||
}
|
||||
|
||||
@objc private func filterTapped() {
|
||||
if isStoreMode {
|
||||
presentStoreFilterMenu()
|
||||
} else {
|
||||
presentPhotographerFilterMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dateTapped() {
|
||||
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 {
|
||||
|
||||
@ -6,39 +6,53 @@
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 订单列表容器,含下拉刷新与分页。
|
||||
/// 订单列表容器,含下拉刷新、空态与分页,对齐 Android LazyColumn。
|
||||
final class OrderListView: UIView {
|
||||
|
||||
enum Mode {
|
||||
case photographer
|
||||
case store
|
||||
}
|
||||
|
||||
var onRefresh: (() -> Void)?
|
||||
var onLoadMore: ((Int) -> Void)?
|
||||
|
||||
let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let refreshControl = UIRefreshControl()
|
||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||
private let emptyLabel = UILabel()
|
||||
private let footerLabel = UILabel()
|
||||
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = UIColor(hex: 0xF5F5F5)
|
||||
backgroundColor = AppColor.pageBackground
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.separatorStyle = .none
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.contentInset = UIEdgeInsets(top: OrderTokens.cardGap, left: 0, bottom: OrderTokens.cardGap, right: 0)
|
||||
tableView.refreshControl = refreshControl
|
||||
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
|
||||
|
||||
footerLabel.font = .systemFont(ofSize: 12)
|
||||
footerLabel.textColor = UIColor(hex: 0x9CA3AF)
|
||||
loadingIndicator.hidesWhenStopped = true
|
||||
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.text = "没有更多"
|
||||
|
||||
footerSpinner.hidesWhenStopped = true
|
||||
footerSpinner.color = AppColor.primary
|
||||
|
||||
addSubview(tableView)
|
||||
addSubview(loadingIndicator)
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
loadingIndicator.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -46,18 +60,54 @@ final class OrderListView: UIView {
|
||||
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 !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
|
||||
} else {
|
||||
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? {
|
||||
guard !canLoadMore || isEmpty else { return nil }
|
||||
private func emptyBackgroundView() -> UIView {
|
||||
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)
|
||||
return footerLabel
|
||||
}
|
||||
|
||||
@ -146,7 +146,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
listView.apply(
|
||||
isRefreshing: depositViewModel.isRefreshing,
|
||||
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 {
|
||||
depositViewModel.dismissWriteOffDialog()
|
||||
@ -162,7 +164,9 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
listView.apply(
|
||||
isRefreshing: orderListViewModel.isRefreshing,
|
||||
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 {
|
||||
orderListViewModel.clearSignInResponse()
|
||||
@ -186,11 +190,11 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
|
||||
private func dateRangeText(start: Date?, end: Date?) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy/MM/dd"
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
if let start, let end {
|
||||
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
|
||||
}
|
||||
return "全部"
|
||||
return "时间筛选"
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
actions.onCallPhone = {
|
||||
guard let url = URL(string: "tel://\(item.phone)") else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
actions.onTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user