Implement orders tab aligned with Android, including scan verify flows.

Add role-based order/deposit lists, AVFoundation QR scanning, verify dialogs, and ViewModel tests so photographers, scenic admins, and store admins match Android behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:59:53 +08:00
parent 6492f80ef0
commit 31d2b6094b
27 changed files with 4226 additions and 17 deletions

View File

@ -0,0 +1,144 @@
//
// DepositOrderCardCell.swift
// suixinkan
//
import SnapKit
import UIKit
struct DepositOrderCardActions {
var onRefund: (() -> Void)?
var onPartialRefund: (() -> Void)?
var onVerify: (() -> Void)?
var onTap: (() -> Void)?
}
/// Android `DepositOrderListScreen`
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 var actions = DepositOrderCardActions()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(item: StoreOrderItem, actions: DepositOrderCardActions) {
self.actions = actions
projectLabel.text = item.projectName.isEmpty ? item.orderTypeLabel : item.projectName
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)
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
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)
}
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))
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)
}
cardView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
}
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)
}
}
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
}
@objc private func refundTapped() { actions.onRefund?() }
@objc private func partialRefundTapped() { actions.onPartialRefund?() }
@objc private func verifyTapped() { actions.onVerify?() }
@objc private func cardTapped() { actions.onTap?() }
}

View File

@ -0,0 +1,88 @@
//
// DepositOrderDetailViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class DepositOrderDetailViewController: BaseViewController, UITableViewDataSource {
private let orderNumber: String
private let orderAPI: OrderAPI
private var detail: StoreOrderDetailData?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
init(orderNumber: String, orderAPI: OrderAPI = NetworkServices.shared.orderAPI) {
self.orderNumber = orderNumber
self.orderAPI = orderAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "订单详情"
}
override func setupUI() {
tableView.dataSource = self
view.addSubview(tableView)
tableView.snp.makeConstraints { $0.edges.equalToSuperview() }
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await loadDetail() }
}
private func loadDetail() async {
let storeId = AppStore.shared.currentStoreId
guard storeId > 0 else {
showToast("店铺信息异常")
return
}
showLoading()
defer { hideLoading() }
do {
detail = try await orderAPI.storeOrderDetail(storeId: storeId, orderNumber: orderNumber)
tableView.reloadData()
} catch {
showToast(error.localizedDescription)
}
}
func numberOfSections(in tableView: UITableView) -> Int { 2 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 { return 6 }
return detail?.multiTravel?.shootingList.count ?? 0
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 0 ? "基本信息" : "拍摄点位"
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
guard let detail else { return cell }
if indexPath.section == 0 {
switch indexPath.row {
case 0: cell.textLabel?.text = "订单号"; cell.detailTextLabel?.text = detail.orderNumber
case 1: cell.textLabel?.text = "项目"; cell.detailTextLabel?.text = detail.projectName
case 2: cell.textLabel?.text = "状态"; cell.detailTextLabel?.text = detail.orderStatusName
case 3: cell.textLabel?.text = "金额"; cell.detailTextLabel?.text = "¥\(detail.actualPayAmount)"
case 4: cell.textLabel?.text = "手机号"; cell.detailTextLabel?.text = OrderPhoneMask.mask(detail.phone)
default: cell.textLabel?.text = "创建时间"; cell.detailTextLabel?.text = detail.createdAt
}
} else if let item = detail.multiTravel?.shootingList[indexPath.row] {
cell.textLabel?.text = item.scenicSpotName
cell.detailTextLabel?.text = item.displayStaffName
}
return cell
}
}

View File

@ -0,0 +1,68 @@
//
// HistoricalShootingInfoViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class HistoricalShootingInfoViewController: BaseViewController, UITableViewDataSource {
private let orderNumber: String
private let orderAPI: OrderAPI
private var response: MultiTravelShootHistoryResponse?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
init(orderNumber: String, orderAPI: OrderAPI = NetworkServices.shared.orderAPI) {
self.orderNumber = orderNumber
self.orderAPI = orderAPI
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setupNavigationBar() {
title = "历史拍摄"
}
override func setupUI() {
tableView.dataSource = self
view.addSubview(tableView)
tableView.snp.makeConstraints { $0.edges.equalToSuperview() }
}
override func viewDidLoad() {
super.viewDidLoad()
Task { await loadData() }
}
private func loadData() async {
showLoading()
defer { hideLoading() }
do {
response = try await orderAPI.shootHistory(orderNumber: orderNumber)
tableView.reloadData()
} catch {
showToast(error.localizedDescription)
}
}
func numberOfSections(in tableView: UITableView) -> Int { 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
response?.photogSpotList.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
guard let spot = response?.photogSpotList[indexPath.row] else { return cell }
cell.textLabel?.text = spot.scenicSpotName
cell.detailTextLabel?.text = "\(spot.photographerDisplayName) · \(spot.files.count)个素材"
cell.accessoryType = .disclosureIndicator
return cell
}
}

View File

@ -0,0 +1,91 @@
//
// MultiVerifyScenicSpotViewController.swift
// suixinkan
//
import SnapKit
import UIKit
/// Sheet
final class MultiVerifyScenicSpotViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var onConfirm: ((MultiTravelScenicSpotEntity) -> Void)?
var onCancel: (() -> Void)?
private var spots: [MultiTravelScenicSpotEntity]
private var selectedSpot: MultiTravelScenicSpotEntity?
private let tableView = UITableView(frame: .zero, style: .plain)
private let confirmButton = UIButton(type: .system)
init(spots: [MultiTravelScenicSpotEntity], selected: MultiTravelScenicSpotEntity?) {
self.spots = spots
self.selectedSpot = selected ?? spots.first
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "选择打卡点"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelTapped))
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "spot")
confirmButton.setTitle("确认核销", for: .normal)
confirmButton.backgroundColor = AppColor.primary
confirmButton.setTitleColor(.white, for: .normal)
confirmButton.layer.cornerRadius = 8
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
view.addSubview(tableView)
view.addSubview(confirmButton)
tableView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.bottom.equalTo(confirmButton.snp.top).offset(-12)
}
confirmButton.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(16)
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(16)
make.height.equalTo(48)
}
}
@objc private func cancelTapped() {
onCancel?()
dismiss(animated: true)
}
@objc private func confirmTapped() {
guard let selectedSpot else { return }
onConfirm?(selectedSpot)
dismiss(animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
spots.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "spot", for: indexPath)
let spot = spots[indexPath.row]
var config = cell.defaultContentConfiguration()
config.text = spot.name
config.secondaryText = "\(spot.scenicName) · \(spot.distanceText)"
cell.contentConfiguration = config
cell.accessoryType = spot.scenicSpotId == selectedSpot?.scenicSpotId ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedSpot = spots[indexPath.row]
tableView.reloadData()
}
}

View File

@ -0,0 +1,314 @@
//
// 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)?
}
/// Android `OrderView`
final class OrderCardCell: UITableViewCell {
static let reuseIdentifier = "OrderCardCell"
private let cardView = UIView()
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 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 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, 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
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
)
refundButton.isHidden = !showRefund
verifyButton.isHidden = !showVerify
actionStack.isHidden = !showRefund && !showVerify
uploadTaskButton.isHidden = hideWriteActions || order.orderType != 19
orderSourceButton.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)
}
if let travel = order.photoTravel {
addInfoRow(title: "修图张数", value: "\(travel.orderPhotoNum)")
addInfoRow(title: "视频", value: "\(travel.orderVideoNum)")
}
if let remark = order.remark, !remark.isEmpty {
addInfoRow(title: "备注", value: remark)
}
let materials = order.multiTravel?.materialList ?? []
historicalButton.isHidden = materials.isEmpty
}
private func setupUI() {
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 12
orderNumberLabel.font = .systemFont(ofSize: 14)
orderNumberLabel.textColor = .black
createdAtLabel.font = .systemFont(ofSize: 12)
createdAtLabel.textColor = UIColor(hex: 0x6B7280)
typeChip.font = .systemFont(ofSize: 12)
typeChip.textColor = AppColor.primary
typeChip.backgroundColor = UIColor(hex: 0xEFF6FF)
typeChip.layer.cornerRadius = 4
typeChip.clipsToBounds = true
typeChip.textAlignment = .center
configureActionButton(refundButton, title: "退款", color: UIColor(hex: 0xEF4444), bg: UIColor(hex: 0xFFE7E7))
configureActionButton(verifyButton, title: "核销订单", color: .white, bg: UIColor(hex: 0x0073FF))
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 = .systemFont(ofSize: 16)
uploadTaskButton.backgroundColor = UIColor(hex: 0xEFF6FF)
uploadTaskButton.layer.cornerRadius = 12
uploadTaskButton.addTarget(self, action: #selector(uploadTaskTapped), for: .touchUpInside)
actionStack.axis = .horizontal
actionStack.spacing = 12
actionStack.distribution = .fillEqually
actionStack.addArrangedSubview(refundButton)
actionStack.addArrangedSubview(verifyButton)
infoStack.axis = .vertical
infoStack.spacing = 6
cancelButton.setTitle("取消订单", for: .normal)
cancelButton.setTitleColor(UIColor(hex: 0xEF4444), for: .normal)
cancelButton.titleLabel?.font = .systemFont(ofSize: 14)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
orderSourceButton.setTitleColor(AppColor.primary, for: .normal)
orderSourceButton.titleLabel?.font = .systemFont(ofSize: 14)
orderSourceButton.contentHorizontalAlignment = .leading
orderSourceButton.addTarget(self, action: #selector(orderSourceTapped), for: .touchUpInside)
historicalButton.setTitle("历史拍摄", for: .normal)
historicalButton.setTitleColor(AppColor.primary, for: .normal)
historicalButton.titleLabel?.font = .systemFont(ofSize: 14)
historicalButton.addTarget(self, action: #selector(historicalTapped), for: .touchUpInside)
refinedStack.axis = .horizontal
refinedStack.spacing = 8
refinedNeedButton.setTitle("需要精修", for: .normal)
refinedNoButton.setTitle("无需精修", for: .normal)
[refinedNeedButton, refinedNoButton].forEach {
$0.titleLabel?.font = .systemFont(ofSize: 13)
$0.layer.cornerRadius = 6
$0.layer.borderWidth = 1
}
refinedNeedButton.addTarget(self, action: #selector(refinedNeedTapped), for: .touchUpInside)
refinedNoButton.addTarget(self, action: #selector(refinedNoTapped), for: .touchUpInside)
refinedStack.addArrangedSubview(refinedNeedButton)
refinedStack.addArrangedSubview(refinedNoButton)
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.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 16, bottom: 12, right: 16))
}
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)
}
}
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: 16)
button.backgroundColor = bg
button.layer.cornerRadius = 8
}
private func addInfoRow(title: String, value: String) {
let row = UIStackView()
row.axis = .horizontal
row.distribution = .fillEqually
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 13)
titleLabel.textColor = UIColor(hex: 0x6B7280)
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 13)
valueLabel.textColor = AppColor.text333
valueLabel.textAlignment = .right
row.addArrangedSubview(titleLabel)
row.addArrangedSubview(valueLabel)
infoStack.addArrangedSubview(row)
}
private func addPhoneRow(phone: String, hidden: Bool) {
let row = UIStackView()
row.axis = .horizontal
row.distribution = .fillEqually
let titleLabel = UILabel()
titleLabel.text = "手机号"
titleLabel.font = .systemFont(ofSize: 13)
titleLabel.textColor = UIColor(hex: 0x6B7280)
let valueButton = UIButton(type: .system)
valueButton.setTitle(OrderPhoneMask.mask(phone), for: .normal)
valueButton.setTitleColor(AppColor.primary, for: .normal)
valueButton.titleLabel?.font = .systemFont(ofSize: 13)
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) {
let selectedBorder = AppColor.primary.cgColor
let normalBorder = UIColor(hex: 0xE5E7EB).cgColor
refinedNeedButton.layer.borderColor = isRefined == 1 ? selectedBorder : normalBorder
refinedNoButton.layer.borderColor = isRefined == 2 ? selectedBorder : normalBorder
refinedNeedButton.isEnabled = !readOnly
refinedNoButton.isEnabled = !readOnly
}
@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 {
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)"
}
}

View File

@ -0,0 +1,95 @@
//
// OrderDialogControllers.swift
// suixinkan
//
import UIKit
enum OrderRefundAlertController {
static func present(
from presenter: UIViewController,
order: OrderEntity,
onConfirm: @escaping (OrderRefundRequest) -> Void
) {
let alert = UIAlertController(title: "订单退款", message: order.orderNumber, preferredStyle: .alert)
alert.addTextField { $0.placeholder = "退款金额"; $0.text = order.actualPayAmount }
alert.addTextField { $0.placeholder = "退款原因" }
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确认退款", style: .destructive) { _ in
let amount = alert.textFields?.first?.text ?? order.actualPayAmount
let reason = alert.textFields?.last?.text ?? ""
onConfirm(OrderRefundRequest(orderNumber: order.orderNumber, refundType: 1, refundAmount: amount, refundReason: reason))
})
presenter.present(alert, animated: true)
}
}
enum DepositOrderRefundAlertController {
static func present(
from presenter: UIViewController,
orderNumber: String,
amount: String,
refundType: Int,
onConfirm: @escaping (String, String) -> Void
) {
let title = refundType == 3 ? "部分退款" : "全额退款"
let alert = UIAlertController(title: title, message: orderNumber, preferredStyle: .alert)
alert.addTextField { $0.placeholder = "退款金额"; $0.text = amount }
alert.addTextField { $0.placeholder = "退款原因" }
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确认", style: .destructive) { _ in
let refundAmount = alert.textFields?.first?.text ?? amount
let reason = alert.textFields?.last?.text ?? ""
onConfirm(refundAmount, reason)
})
presenter.present(alert, animated: true)
}
}
enum OrderSignInAlertController {
static func present(from presenter: UIViewController, response: OrderSignInResponse) {
let message = """
项目:\(response.projectName)
景区:\(response.scenicName)
金额:¥\(response.orderAmount)
手机号:\(response.userPhone)
"""
let alert = UIAlertController(title: response.checkIn ? "签到成功" : "核销结果", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
presenter.present(alert, animated: true)
}
}
enum WriteOffResultAlertController {
static func present(from presenter: UIViewController, response: WriteOffOrderResponse) {
let message = """
项目:\(response.projectName)
景区:\(response.scenicName)
金额:¥\(response.orderAmount)
手机号:\(response.userPhone)
"""
let alert = UIAlertController(title: response.orderVerify ? "核销成功" : "核销结果", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
presenter.present(alert, animated: true)
}
}
enum SendGiftAlertController {
static func present(
from presenter: UIViewController,
photoNum: Int,
videoNum: Int,
onConfirm: @escaping (Int, Int) -> Void
) {
let alert = UIAlertController(title: "赠送修图", message: nil, preferredStyle: .alert)
alert.addTextField { $0.placeholder = "赠送照片数"; $0.keyboardType = .numberPad; $0.text = "\(photoNum)" }
alert.addTextField { $0.placeholder = "赠送视频数"; $0.keyboardType = .numberPad; $0.text = "\(videoNum)" }
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "确认", style: .default) { _ in
let photo = Int(alert.textFields?.first?.text ?? "0") ?? 0
let video = Int(alert.textFields?.last?.text ?? "0") ?? 0
onConfirm(photo, video)
})
presenter.present(alert, animated: true)
}
}

View File

@ -0,0 +1,134 @@
//
// OrderFilterBarView.swift
// suixinkan
//
import SnapKit
import UIKit
/// + +
final class OrderFilterBarView: UIView {
var onSearchChanged: ((String) -> Void)?
var onSearchSubmitted: (() -> Void)?
var onFilterSelected: ((Int, Int) -> Void)?
var onDateRangeTap: (() -> Void)?
private let searchField = UITextField()
private let filterButton = UIButton(type: .system)
private let dateButton = UIButton(type: .system)
private var isStoreMode = false
private var currentFilterLabel = "全部"
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupUI()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(isStoreMode: Bool, filterLabel: String, dateRangeText: String, searchText: String) {
self.isStoreMode = isStoreMode
currentFilterLabel = filterLabel
searchField.placeholder = isStoreMode ? "输入游客手机号" : "输入手机号搜索"
searchField.text = searchText
filterButton.setTitle("\(filterLabel)", for: .normal)
dateButton.setTitle(dateRangeText, for: .normal)
}
private func setupUI() {
searchField.font = .systemFont(ofSize: 14)
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.delegate = self
filterButton.setTitleColor(AppColor.text333, for: .normal)
filterButton.titleLabel?.font = .systemFont(ofSize: 14)
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
dateButton.setTitleColor(AppColor.primary, for: .normal)
dateButton.titleLabel?.font = .systemFont(ofSize: 14)
dateButton.contentHorizontalAlignment = .trailing
dateButton.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
addSubview(searchField)
addSubview(filterButton)
addSubview(dateButton)
searchField.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(16)
make.height.equalTo(36)
}
filterButton.snp.makeConstraints { make in
make.top.equalTo(searchField.snp.bottom).offset(12)
make.leading.equalToSuperview().inset(16)
make.bottom.equalToSuperview().inset(12)
}
dateButton.snp.makeConstraints { make in
make.centerY.equalTo(filterButton)
make.trailing.equalToSuperview().inset(16)
}
}
@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 {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
onSearchSubmitted?()
return true
}
}

View File

@ -0,0 +1,73 @@
//
// OrderListView.swift
// suixinkan
//
import SnapKit
import UIKit
///
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 footerLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(hex: 0xF5F5F5)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.keyboardDismissMode = .onDrag
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(refreshTriggered), for: .valueChanged)
footerLabel.font = .systemFont(ofSize: 12)
footerLabel.textColor = UIColor(hex: 0x9CA3AF)
footerLabel.textAlignment = .center
footerLabel.text = "没有更多"
addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(isRefreshing: Bool, canLoadMore: Bool, isEmpty: Bool) {
if isRefreshing {
if !refreshControl.isRefreshing { refreshControl.beginRefreshing() }
} else {
refreshControl.endRefreshing()
}
footerLabel.text = isEmpty ? "暂无订单" : (canLoadMore ? "" : "没有更多")
tableView.tableFooterView = footerLabelFrame(isEmpty: isEmpty, canLoadMore: canLoadMore)
}
private func footerLabelFrame(isEmpty: Bool, canLoadMore: Bool) -> UIView? {
guard !canLoadMore || isEmpty else { return nil }
footerLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 44)
return footerLabel
}
@objc private func refreshTriggered() {
onRefresh?()
}
func notifyLoadMoreIfNeeded(at index: Int, shouldLoad: Bool) {
guard shouldLoad else { return }
onLoadMore?(index)
}
}

View File

@ -0,0 +1,76 @@
//
// OrderSourcePickerViewController.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class OrderSourcePickerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var onPersonSelected: ((OrderSourcePerson) -> Void)?
var onLeadSelected: ((OrderSourceSelection) -> Void)?
private let people: [OrderSourcePerson]
private let leads: [OrderSourceLead]
private let selectedPerson: OrderSourcePerson?
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
private var showingLeads = false
init(people: [OrderSourcePerson], leads: [OrderSourceLead], selectedPerson: OrderSourcePerson?) {
self.people = people
self.leads = leads
self.selectedPerson = selectedPerson
self.showingLeads = !leads.isEmpty
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = showingLeads ? "选择带客单" : "选择获客员"
view.backgroundColor = UIColor(hex: 0xF5F5F5)
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "关闭", style: .plain, target: self, action: #selector(closeTapped))
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.snp.makeConstraints { $0.edges.equalToSuperview() }
}
@objc private func closeTapped() {
dismiss(animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
showingLeads ? leads.count : people.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
if showingLeads {
let lead = leads[indexPath.row]
cell.textLabel?.text = lead.displayText
cell.detailTextLabel?.text = lead.userMobile
} else {
let person = people[indexPath.row]
cell.textLabel?.text = person.name
cell.detailTextLabel?.text = person.phone
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if showingLeads, let person = selectedPerson {
let lead = leads[indexPath.row]
onLeadSelected?(OrderSourceSelection(person: person, lead: lead))
dismiss(animated: true)
} else {
onPersonSelected?(people[indexPath.row])
}
}
}

View File

@ -0,0 +1,47 @@
//
// OrderStatusBadgeView.swift
// suixinkan
//
import SnapKit
import UIKit
///
final class OrderStatusBadgeView: UIView {
private let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textAlignment = .center
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4))
}
layer.cornerRadius = 4
clipsToBounds = true
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func apply(status: Int, text: String) {
label.text = text
switch status {
case 18, 20:
label.textColor = UIColor(hex: 0x0073FF)
backgroundColor = UIColor(hex: 0xEFF6FF)
case 30:
label.textColor = UIColor(hex: 0x10B981)
backgroundColor = UIColor(hex: 0xF0FDF4)
case 50:
label.textColor = UIColor(hex: 0xEF4444)
backgroundColor = UIColor(hex: 0xFFE7E7)
default:
label.textColor = UIColor(hex: 0xFF7B00)
backgroundColor = UIColor(hex: 0xFFF0E2)
}
}
}

View File

@ -3,26 +3,476 @@
// suixinkan
//
import CoreLocation
import SnapKit
import UIKit
/// Tab
final class OrdersViewController: BaseViewController {
/// Tab //
final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
private let titleLabel = UILabel()
private let orderAPI = NetworkServices.shared.orderAPI
private let orderListViewModel = OrderListViewModel()
private let depositViewModel = DepositOrderListViewModel()
private let filterBar = OrderFilterBarView()
private let listView = OrderListView()
private var hasInitialized = false
private var isStoreMode: Bool { AppStore.shared.currentAppRole == .storeAdmin }
private var pendingOrderSourceNumber: String?
private var pendingOrderSourcePerson: OrderSourcePerson?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider = OrderLocationProvider()
override func setupNavigationBar() {
title = "订单"
}
override func setupUI() {
title = "订单"
titleLabel.text = "订单"
titleLabel.font = .systemFont(ofSize: 24, weight: .medium)
titleLabel.textAlignment = .center
titleLabel.textColor = AppColor.text333
view.addSubview(titleLabel)
view.backgroundColor = UIColor(hex: 0xF5F5F5)
view.addSubview(filterBar)
view.addSubview(listView)
listView.tableView.dataSource = self
listView.tableView.delegate = self
listView.tableView.register(OrderCardCell.self, forCellReuseIdentifier: OrderCardCell.reuseIdentifier)
listView.tableView.register(DepositOrderCardCell.self, forCellReuseIdentifier: DepositOrderCardCell.reuseIdentifier)
}
override func setupConstraints() {
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
filterBar.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
}
listView.snp.makeConstraints { make in
make.top.equalTo(filterBar.snp.bottom)
make.leading.trailing.bottom.equalToSuperview()
}
}
override func bindActions() {
orderListViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
orderListViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
depositViewModel.onStateChange = { [weak self] in
Task { @MainActor in self?.applyViewModel() }
}
depositViewModel.onShowMessage = { [weak self] message in
Task { @MainActor in self?.showToast(message) }
}
filterBar.onSearchChanged = { [weak self] text in
guard let self else { return }
if self.isStoreMode {
self.depositViewModel.onSearchTextChange(text, api: self.orderAPI)
} else {
self.orderListViewModel.onSearchTextChange(text)
}
}
filterBar.onSearchSubmitted = { [weak self] in
guard let self else { return }
Task {
if self.isStoreMode {
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
filterBar.onFilterSelected = { [weak self] tab, rTab in
guard let self else { return }
Task {
if self.isStoreMode {
self.depositViewModel.onFilterChange(tab: tab)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onFilterChange(orderStatus: tab, isRefined: rTab)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
filterBar.onDateRangeTap = { [weak self] in
self?.presentDateRangeSheet()
}
listView.onRefresh = { [weak self] in
guard let self else { return }
Task {
if self.isStoreMode {
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
}
}
listView.onLoadMore = { [weak self] index in
guard let self else { return }
Task {
if self.isStoreMode {
guard self.depositViewModel.shouldLoadMore(at: index) else { return }
await self.depositViewModel.loadMore(api: self.orderAPI)
} else {
guard self.orderListViewModel.shouldLoadMore(at: index) else { return }
await self.orderListViewModel.loadMore(api: self.orderAPI)
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard !hasInitialized else { return }
hasInitialized = true
Task { await initializeList() }
}
private func initializeList() async {
if isStoreMode {
await depositViewModel.refreshOrderList(api: orderAPI)
} else {
await orderListViewModel.refreshOrderList(api: orderAPI)
}
}
private func applyViewModel() {
if isStoreMode {
filterBar.configure(
isStoreMode: true,
filterLabel: depositViewModel.filterLabel,
dateRangeText: dateRangeText(start: depositViewModel.startTime, end: depositViewModel.endTime),
searchText: depositViewModel.searchText
)
listView.apply(
isRefreshing: depositViewModel.isRefreshing,
canLoadMore: depositViewModel.canLoadMore,
isEmpty: depositViewModel.orderList.isEmpty
)
if let response = depositViewModel.writeOffResponse {
depositViewModel.dismissWriteOffDialog()
WriteOffResultAlertController.present(from: self, response: response)
}
} else {
filterBar.configure(
isStoreMode: false,
filterLabel: orderListViewModel.filterLabel,
dateRangeText: dateRangeText(start: orderListViewModel.startTime, end: orderListViewModel.endTime),
searchText: orderListViewModel.searchText
)
listView.apply(
isRefreshing: orderListViewModel.isRefreshing,
canLoadMore: orderListViewModel.canLoadMore,
isEmpty: orderListViewModel.orderList.isEmpty
)
if let response = orderListViewModel.signInResponse {
orderListViewModel.clearSignInResponse()
OrderSignInAlertController.present(from: self, response: response)
}
if let response = orderListViewModel.writeOffResponse {
orderListViewModel.clearWriteOffResponse()
WriteOffResultAlertController.present(from: self, response: response)
}
if orderListViewModel.multiVerifyPendingData != nil,
orderListViewModel.multiVerifyScenicSpotList.isEmpty,
!isFetchingMultiVerifySpots {
Task { await self.fetchMultiVerifySpotsIfNeeded() }
}
if !orderListViewModel.multiVerifyScenicSpotList.isEmpty {
presentMultiVerifySheet()
}
}
listView.tableView.reloadData()
}
private func dateRangeText(start: Date?, end: Date?) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
if let start, let end {
return "\(formatter.string(from: start))-\(formatter.string(from: end))"
}
return "全部"
}
private func presentDateRangeSheet() {
let start = isStoreMode ? depositViewModel.startTime : orderListViewModel.startTime
let end = isStoreMode ? depositViewModel.endTime : orderListViewModel.endTime
let controller = DateRangePickerViewController(rangeStart: start, rangeEnd: end)
controller.onShortcutSelected = { [weak self, weak controller] shortcut in
guard let self else { return }
let range = self.resolveShortcutRange(shortcut)
Task {
if self.isStoreMode {
self.depositViewModel.onDateRangeSelected(start: range.start, end: range.end)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onDateRangeSelected(start: range.start, end: range.end)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
controller?.dismiss(animated: true)
}
}
controller.onRangeSelected = { [weak self, weak controller] start, end in
guard let self else { return }
Task {
if self.isStoreMode {
self.depositViewModel.onDateRangeSelected(start: start, end: end)
await self.depositViewModel.refreshOrderList(api: self.orderAPI)
} else {
self.orderListViewModel.onDateRangeSelected(start: start, end: end)
await self.orderListViewModel.refreshOrderList(api: self.orderAPI)
}
controller?.dismiss(animated: true)
}
}
if let sheet = controller.sheetPresentationController {
sheet.detents = [.medium(), .large()]
}
present(controller, animated: true)
}
private func resolveShortcutRange(_ shortcut: String) -> (start: Date?, end: Date?) {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
switch shortcut {
case StatisticsRangeShortcut.today.rawValue:
return (today, today)
case StatisticsRangeShortcut.yesterday.rawValue:
let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
return (yesterday, yesterday)
case StatisticsRangeShortcut.last7Days.rawValue:
let start = calendar.date(byAdding: .day, value: -6, to: today)!
return (start, today)
default:
return (nil, nil)
}
}
private func presentScanner(onResult: @escaping (String) -> Void) {
let scanner = QRCodeScannerViewController()
scanner.onScanResult = onResult
let nav = UINavigationController(rootViewController: scanner)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
}
private func fetchMultiVerifySpotsIfNeeded() async {
isFetchingMultiVerifySpots = true
defer { isFetchingMultiVerifySpots = false }
do {
let coordinate = try await locationProvider.requestCoordinate()
await orderListViewModel.fetchScenicSpotsAndShowSheet(
lat: coordinate.latitude,
lng: coordinate.longitude,
api: orderAPI
)
applyViewModel()
} catch {
showToast(error.localizedDescription)
orderListViewModel.clearMultiVerifyPending()
}
}
private func presentMultiVerifySheet() {
let spots = orderListViewModel.multiVerifyScenicSpotList
guard !spots.isEmpty else { return }
let controller = MultiVerifyScenicSpotViewController(
spots: spots,
selected: orderListViewModel.selectedMultiVerifySpot
)
controller.onConfirm = { [weak self] spot in
guard let self else { return }
self.orderListViewModel.onMultiVerifySpotSelected(spot)
Task { await self.orderListViewModel.onMultiVerifyConfirm(api: self.orderAPI) }
}
controller.onCancel = { [weak self] in
self?.orderListViewModel.clearMultiVerifySheet()
}
let nav = UINavigationController(rootViewController: controller)
if let sheet = nav.sheetPresentationController {
sheet.detents = [.medium(), .large()]
}
present(nav, animated: true)
orderListViewModel.clearMultiVerifySheet()
}
// MARK: - UITableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
isStoreMode ? depositViewModel.orderList.count : orderListViewModel.orderList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if isStoreMode {
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier, for: indexPath) as! DepositOrderCardCell
let item = depositViewModel.orderList[indexPath.row]
cell.configure(item: item, actions: makeDepositActions(for: item))
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: depositViewModel.shouldLoadMore(at: indexPath.row))
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier, for: indexPath) as! OrderCardCell
let order = orderListViewModel.orderList[indexPath.row]
let source = orderListViewModel.orderSourceSelection(for: order)
cell.configure(
order: order,
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
actions: makeOrderActions(for: order),
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
sourceText: source.map { "\($0.person.name) · \($0.lead.displayText)" }
)
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
return cell
}
private func makeOrderActions(for order: OrderEntity) -> OrderCardActions {
var actions = OrderCardActions()
actions.onRefund = { [weak self] in
guard let self else { return }
OrderRefundAlertController.present(from: self, order: order) { request in
Task { await self.orderListViewModel.refundOrder(request, api: self.orderAPI) }
}
}
actions.onVerify = { [weak self] in
self?.presentScanner { result in
Task { await self?.orderListViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
}
}
actions.onUploadTask = { [weak self] in
self?.showToast("上传任务待接入")
}
actions.onCancel = { [weak self] in
guard let self else { return }
Task { await self.orderListViewModel.cancelOrder(orderNumber: order.orderNumber, api: self.orderAPI) }
}
actions.onCallPhone = {
guard let url = URL(string: "tel://\(order.phone)") else { return }
UIApplication.shared.open(url)
}
actions.onRefinedChanged = { [weak self] value in
guard let self else { return }
Task { await self.orderListViewModel.updateRefined(orderNumber: order.orderNumber, isRefined: value, api: self.orderAPI) }
}
actions.onHistoricalPhoto = { [weak self] in
let controller = HistoricalShootingInfoViewController(orderNumber: order.orderNumber)
self?.navigationController?.pushViewController(controller, animated: true)
}
actions.onOrderSourceTap = { [weak self] in
self?.presentOrderSourcePicker(for: order)
}
if let travel = order.photoTravel, travel.canGiftRetouch {
actions.onSendGift = { [weak self] in
guard let self else { return }
self.pendingGiftOrderNumber = order.orderNumber
SendGiftAlertController.present(
from: self,
photoNum: travel.retouchGiftPhotoNum,
videoNum: travel.retouchGiftVideoNum
) { photo, video in
Task {
await self.orderListViewModel.sendGift(
orderNumber: order.orderNumber,
photoNum: photo,
videoNum: video,
api: self.orderAPI
)
}
}
}
}
return actions
}
private func makeDepositActions(for item: StoreOrderItem) -> DepositOrderCardActions {
var actions = DepositOrderCardActions()
actions.onRefund = { [weak self] in
guard let self else { return }
DepositOrderRefundAlertController.present(
from: self,
orderNumber: item.orderNumber,
amount: item.actualPayAmount,
refundType: 1
) { amount, reason in
Task {
await self.depositViewModel.requestRefund(
orderNumber: item.orderNumber,
refundType: 1,
refundAmount: amount,
refundReason: reason,
api: self.orderAPI
)
}
}
}
actions.onPartialRefund = { [weak self] in
guard let self else { return }
DepositOrderRefundAlertController.present(
from: self,
orderNumber: item.orderNumber,
amount: item.actualPayAmount,
refundType: 3
) { amount, reason in
Task {
await self.depositViewModel.requestRefund(
orderNumber: item.orderNumber,
refundType: 3,
refundAmount: amount,
refundReason: reason,
api: self.orderAPI
)
}
}
}
actions.onVerify = { [weak self] in
self?.presentScanner { result in
Task { await self?.depositViewModel.processScanResult(result, api: self?.orderAPI ?? NetworkServices.shared.orderAPI) }
}
}
actions.onTap = { [weak self] in
guard let self else { return }
guard self.depositViewModel.canOpenDetail(for: item) else {
self.showToast("该订单类型暂无详情")
return
}
let controller = DepositOrderDetailViewController(orderNumber: item.orderNumber)
self.navigationController?.pushViewController(controller, animated: true)
}
return actions
}
private func presentOrderSourcePicker(for order: OrderEntity) {
pendingOrderSourceNumber = order.orderNumber
Task {
await orderListViewModel.onOrderSourceSheetOpen(api: orderAPI)
let controller = OrderSourcePickerViewController(
people: orderListViewModel.orderSourcePeople,
leads: [],
selectedPerson: nil
)
controller.onPersonSelected = { [weak self] person in
guard let self else { return }
self.pendingOrderSourcePerson = person
Task {
await self.orderListViewModel.loadOrderSourceLeads(person: person, api: self.orderAPI)
let leadsController = OrderSourcePickerViewController(
people: self.orderListViewModel.orderSourcePeople,
leads: self.orderListViewModel.orderSourceLeads[person.key] ?? [],
selectedPerson: person
)
leadsController.onLeadSelected = { [weak self] selection in
guard let self, let orderNumber = self.pendingOrderSourceNumber else { return }
Task {
await self.orderListViewModel.bindOrderSource(
orderNumber: orderNumber,
selection: selection,
api: self.orderAPI
)
}
}
self.present(leadsController, animated: true)
}
}
present(controller, animated: true)
}
}
}