// // OrderCardCell.swift // suixinkan // import SnapKit import UIKit struct OrderCardActions { var onRefund: (() -> Void)? var onVerify: (() -> Void)? var onUploadTask: (() -> Void)? var onCancel: (() -> Void)? var onCallPhone: (() -> Void)? var onGiftPhotoMinus: (() -> Void)? var onGiftPhotoPlus: (() -> Void)? var onGiftVideoMinus: (() -> Void)? var onGiftVideoPlus: (() -> Void)? var onSendGift: (() -> Void)? var onRefinedChanged: ((Int) -> Void)? var onHistoricalPhoto: (() -> Void)? var onOrderSourceTap: (() -> Void)? var onOrderSourceImageTap: (([String], Int) -> Void)? } /// 摄影师订单卡片,严格对齐 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 = OrderTypeChipView() private let orderSourceEntryView = OrderSourceEntryView() 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?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.backgroundColor = .clear setupUI() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure( order: OrderEntity, hideWriteActions: Bool, actions: OrderCardActions, showOrderSource: Bool, sourceSelection: OrderSourceSelection? ) { self.actions = actions orderNumberLabel.text = "订单号:\(order.orderNumber)" statusBadge.apply(status: order.orderStatus, text: order.orderStatusName) createdAtLabel.text = order.createdAt.isEmpty ? "--" : order.createdAt 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 updateRefundWidth(showVerify: showVerify && showRefund) uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19 orderSourceEntryView.isHidden = !showOrderSource if showOrderSource { orderSourceEntryView.apply(selection: sourceSelection) } let showEdit = (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19 editOptionView.isHidden = !showEdit if showEdit { editOptionView.apply(isRefined: order.isRefined, readOnly: hideWriteActions) } 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 { remarkSection.isHidden = false remarkBodyLabel.text = remark } else { remarkSection.isHidden = true } let materials = order.multiTravel?.materialList ?? [] 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 = OrderTokens.cardRadius orderNumberLabel.font = .app(.body) orderNumberLabel.textColor = OrderTokens.orderNumber createdAtLabel.font = .app(.caption) createdAtLabel.textColor = OrderTokens.timestamp headerRow.axis = .horizontal headerRow.alignment = .center headerRow.distribution = .equalSpacing headerRow.addArrangedSubview(orderNumberLabel) headerRow.addArrangedSubview(statusBadge) orderSourceEntryView.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside) orderSourceEntryView.onImageTap = { [weak self] images, index in self?.actions.onOrderSourceImageTap?(images, index) } 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, orderSourceEntryView, actionRow, uploadTaskButton, editOptionView, refundAmountRow, detailStack, photoCountRow, videoCountRow, remarkSection, historicalPhotosView, cancelButton, ].forEach { contentStack.addArrangedSubview($0) } contentView.addSubview(cardView) cardView.addSubview(contentStack) cardView.snp.makeConstraints { make in make.edges.equalToSuperview().inset( UIEdgeInsets(top: 0, left: AppSpacing.md, bottom: OrderTokens.cardGap, right: AppSpacing.md) ) } contentStack.snp.makeConstraints { make in make.edges.equalToSuperview().inset(OrderTokens.cardPadding) } } private func configureTravelRow( _ row: UIStackView, titleLabel: UILabel, valueLabel: UILabel, title: String, stepper: OrderGiftStepperView ) { row.axis = .horizontal row.alignment = .center row.distribution = .equalSpacing titleLabel.text = title titleLabel.font = .app(.body) titleLabel.textColor = OrderTokens.label563 valueLabel.font = .systemFont(ofSize: 14, weight: .medium) valueLabel.textColor = OrderTokens.valueBlack row.addArrangedSubview(titleLabel) row.addArrangedSubview(stepper) row.addArrangedSubview(valueLabel) valueLabel.isHidden = true } 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 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 orderSourceTapped() { actions.onOrderSourceTap?() } } enum OrderPhoneMask { static func mask(_ phone: String) -> String { let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.count >= 7 else { return trimmed } let start = trimmed.prefix(3) let end = trimmed.suffix(4) return "\(start)****\(end)" } }